From 6d7f2895b7bf0866fe2cdeedd2e8056fb9373c04 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Thu, 6 Nov 2025 10:52:28 +0000 Subject: [PATCH 01/41] Created `@fedify/lint` package --- packages/lint/deno.json | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 packages/lint/deno.json diff --git a/packages/lint/deno.json b/packages/lint/deno.json new file mode 100644 index 000000000..888e85b0f --- /dev/null +++ b/packages/lint/deno.json @@ -0,0 +1,9 @@ +{ + "name": "@fedify/fedify", + "version": "2.0.0", + "license": "MIT", + "exports": { + ".": "./src/mod.ts" + }, + "imports": {} +} From e2abbf3c072d2e0997b74e00a1a68a15972b8d3a Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Thu, 6 Nov 2025 10:52:45 +0000 Subject: [PATCH 02/41] Added `plugin` --- packages/lint/src/mod.ts | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/lint/src/mod.ts diff --git a/packages/lint/src/mod.ts b/packages/lint/src/mod.ts new file mode 100644 index 000000000..10b03e5f1 --- /dev/null +++ b/packages/lint/src/mod.ts @@ -0,0 +1,5 @@ +const plugin: Deno.lint.Plugin = { + name: "@fedify/lint", + rules: {}, +}; +export default plugin; From d1a6bc891bc9bb86a9d14ddd4a417106fb90641c Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Thu, 6 Nov 2025 10:55:25 +0000 Subject: [PATCH 03/41] Created placeholders --- packages/lint/src/mod.ts | 63 ++++++++++++++++++- .../rules/no-deprecated-handle-property.ts | 2 + .../rules/no-deprecated-handle-variable.ts | 2 + .../lint/src/rules/no-duplicate-dispatcher.ts | 2 + .../src/rules/no-duplicate-inbox-listeners.ts | 2 + .../rules/no-memory-kv-store-in-production.ts | 2 + .../no-recursive-context-method-calls.ts | 2 + .../lint/src/rules/require-activity-actor.ts | 2 + .../lint/src/rules/require-activity-id.ts | 2 + .../lint/src/rules/require-activity-to.ts | 2 + .../src/rules/require-actor-dispatcher.ts | 2 + packages/lint/src/rules/require-actor-id.ts | 2 + .../src/rules/require-actor-return-value.ts | 2 + ...collection-property-when-dispatcher-set.ts | 2 + .../src/rules/require-followers-counter.ts | 2 + .../lint/src/rules/require-inbox-listeners.ts | 2 + packages/lint/src/rules/require-inbox-uri.ts | 2 + .../src/rules/require-integer-timestamp.ts | 2 + .../lint/src/rules/require-key-public-key.ts | 2 + .../src/rules/require-matching-actor-id.ts | 2 + .../rules/require-matching-collection-ids.ts | 2 + .../src/rules/require-matching-inbox-paths.ts | 2 + .../rules/require-message-queue-for-inbox.ts | 2 + .../require-pagination-for-collections.ts | 2 + .../src/rules/require-persistent-kv-store.ts | 2 + .../src/rules/require-signature-fields.ts | 2 + .../rules/require-signature-verification.ts | 2 + ...quire-type-guard-for-activity-listeners.ts | 2 + .../require-valid-uri-template-variables.ts | 2 + 29 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 packages/lint/src/rules/no-deprecated-handle-property.ts create mode 100644 packages/lint/src/rules/no-deprecated-handle-variable.ts create mode 100644 packages/lint/src/rules/no-duplicate-dispatcher.ts create mode 100644 packages/lint/src/rules/no-duplicate-inbox-listeners.ts create mode 100644 packages/lint/src/rules/no-memory-kv-store-in-production.ts create mode 100644 packages/lint/src/rules/no-recursive-context-method-calls.ts create mode 100644 packages/lint/src/rules/require-activity-actor.ts create mode 100644 packages/lint/src/rules/require-activity-id.ts create mode 100644 packages/lint/src/rules/require-activity-to.ts create mode 100644 packages/lint/src/rules/require-actor-dispatcher.ts create mode 100644 packages/lint/src/rules/require-actor-id.ts create mode 100644 packages/lint/src/rules/require-actor-return-value.ts create mode 100644 packages/lint/src/rules/require-collection-property-when-dispatcher-set.ts create mode 100644 packages/lint/src/rules/require-followers-counter.ts create mode 100644 packages/lint/src/rules/require-inbox-listeners.ts create mode 100644 packages/lint/src/rules/require-inbox-uri.ts create mode 100644 packages/lint/src/rules/require-integer-timestamp.ts create mode 100644 packages/lint/src/rules/require-key-public-key.ts create mode 100644 packages/lint/src/rules/require-matching-actor-id.ts create mode 100644 packages/lint/src/rules/require-matching-collection-ids.ts create mode 100644 packages/lint/src/rules/require-matching-inbox-paths.ts create mode 100644 packages/lint/src/rules/require-message-queue-for-inbox.ts create mode 100644 packages/lint/src/rules/require-pagination-for-collections.ts create mode 100644 packages/lint/src/rules/require-persistent-kv-store.ts create mode 100644 packages/lint/src/rules/require-signature-fields.ts create mode 100644 packages/lint/src/rules/require-signature-verification.ts create mode 100644 packages/lint/src/rules/require-type-guard-for-activity-listeners.ts create mode 100644 packages/lint/src/rules/require-valid-uri-template-variables.ts diff --git a/packages/lint/src/mod.ts b/packages/lint/src/mod.ts index 10b03e5f1..c692024a8 100644 --- a/packages/lint/src/mod.ts +++ b/packages/lint/src/mod.ts @@ -1,5 +1,66 @@ +import noDeprecatedHandleProperty from "./rules/no-deprecated-handle-property.ts"; +import noDeprecatedHandleVariable from "./rules/no-deprecated-handle-variable.ts"; +import noDuplicateDispatcher from "./rules/no-duplicate-dispatcher.ts"; +import noDuplicateInboxListeners from "./rules/no-duplicate-inbox-listeners.ts"; +import noMemoryKvStoreInProduction from "./rules/no-memory-kv-store-in-production.ts"; +import noRecursiveContextMethodCalls from "./rules/no-recursive-context-method-calls.ts"; +import requireActivityActor from "./rules/require-activity-actor.ts"; +import requireActivityId from "./rules/require-activity-id.ts"; +import requireActivityTo from "./rules/require-activity-to.ts"; +import requireActorDispatcher from "./rules/require-actor-dispatcher.ts"; +import requireActorId from "./rules/require-actor-id.ts"; +import requireActorReturnValue from "./rules/require-actor-return-value.ts"; +import requireCollectionPropertyWhenDispatcherSet from "./rules/require-collection-property-when-dispatcher-set.ts"; +import requireFollowersCounter from "./rules/require-followers-counter.ts"; +import requireInboxListeners from "./rules/require-inbox-listeners.ts"; +import requireInboxUri from "./rules/require-inbox-uri.ts"; +import requireIntegerTimestamp from "./rules/require-integer-timestamp.ts"; +import requireKeyPublicKey from "./rules/require-key-public-key.ts"; +import requireMatchingActorId from "./rules/require-matching-actor-id.ts"; +import requireMatchingCollectionIds from "./rules/require-matching-collection-ids.ts"; +import requireMatchingInboxPaths from "./rules/require-matching-inbox-paths.ts"; +import requireMessageQueueForInbox from "./rules/require-message-queue-for-inbox.ts"; +import requirePaginationForCollections from "./rules/require-pagination-for-collections.ts"; +import requirePersistentKvStore from "./rules/require-persistent-kv-store.ts"; +import requireSignatureFields from "./rules/require-signature-fields.ts"; +import requireSignatureVerification from "./rules/require-signature-verification.ts"; +import requireTypeGuardForActivityListeners from "./rules/require-type-guard-for-activity-listeners.ts"; +import requireValidUriTemplateVariables from "./rules/require-valid-uri-template-variables.ts"; + const plugin: Deno.lint.Plugin = { name: "@fedify/lint", - rules: {}, + rules: { + "require-actor-dispatcher": requireActorDispatcher, + "require-inbox-listeners": requireInboxListeners, + "require-signature-verification": requireSignatureVerification, + "require-integer-timestamp": requireIntegerTimestamp, + "require-signature-fields": requireSignatureFields, + "require-key-public-key": requireKeyPublicKey, + "require-matching-actor-id": requireMatchingActorId, + "require-matching-collection-ids": requireMatchingCollectionIds, + "no-deprecated-handle-variable": noDeprecatedHandleVariable, + "no-deprecated-handle-property": noDeprecatedHandleProperty, + "no-duplicate-dispatcher": noDuplicateDispatcher, + "no-duplicate-inbox-listeners": noDuplicateInboxListeners, + "require-valid-uri-template-variables": requireValidUriTemplateVariables, + "require-matching-inbox-paths": requireMatchingInboxPaths, + "require-actor-return-value": requireActorReturnValue, + "no-recursive-context-method-calls": noRecursiveContextMethodCalls, + "require-type-guard-for-activity-listeners": + requireTypeGuardForActivityListeners, + "require-collection-property-when-dispatcher-set": + requireCollectionPropertyWhenDispatcherSet, + "require-actor-id": requireActorId, + "require-activity-actor": requireActivityActor, + "require-activity-id": requireActivityId, + "require-activity-to": requireActivityTo, + "require-inbox-uri": requireInboxUri, + "no-memory-kv-store-in-production": noMemoryKvStoreInProduction, + "require-message-queue-for-inbox": requireMessageQueueForInbox, + "require-persistent-kv-store": requirePersistentKvStore, + "require-pagination-for-collections": requirePaginationForCollections, + "require-followers-counter": requireFollowersCounter, + }, }; + export default plugin; diff --git a/packages/lint/src/rules/no-deprecated-handle-property.ts b/packages/lint/src/rules/no-deprecated-handle-property.ts new file mode 100644 index 000000000..f106a1d05 --- /dev/null +++ b/packages/lint/src/rules/no-deprecated-handle-property.ts @@ -0,0 +1,2 @@ +const noDeprecatedHandleProperty: Deno.lint.Rule = {}; +export default noDeprecatedHandleProperty; diff --git a/packages/lint/src/rules/no-deprecated-handle-variable.ts b/packages/lint/src/rules/no-deprecated-handle-variable.ts new file mode 100644 index 000000000..820bc448a --- /dev/null +++ b/packages/lint/src/rules/no-deprecated-handle-variable.ts @@ -0,0 +1,2 @@ +const noDeprecatedHandleVariable: Deno.lint.Rule = {}; +export default noDeprecatedHandleVariable; diff --git a/packages/lint/src/rules/no-duplicate-dispatcher.ts b/packages/lint/src/rules/no-duplicate-dispatcher.ts new file mode 100644 index 000000000..f66408dfe --- /dev/null +++ b/packages/lint/src/rules/no-duplicate-dispatcher.ts @@ -0,0 +1,2 @@ +const noDuplicateDispatcher: Deno.lint.Rule = {}; +export default noDuplicateDispatcher; diff --git a/packages/lint/src/rules/no-duplicate-inbox-listeners.ts b/packages/lint/src/rules/no-duplicate-inbox-listeners.ts new file mode 100644 index 000000000..f6301f2af --- /dev/null +++ b/packages/lint/src/rules/no-duplicate-inbox-listeners.ts @@ -0,0 +1,2 @@ +const noDuplicateInboxListeners: Deno.lint.Rule = {}; +export default noDuplicateInboxListeners; diff --git a/packages/lint/src/rules/no-memory-kv-store-in-production.ts b/packages/lint/src/rules/no-memory-kv-store-in-production.ts new file mode 100644 index 000000000..8aabb4484 --- /dev/null +++ b/packages/lint/src/rules/no-memory-kv-store-in-production.ts @@ -0,0 +1,2 @@ +const noMemoryKvStoreInProduction: Deno.lint.Rule = {}; +export default noMemoryKvStoreInProduction; diff --git a/packages/lint/src/rules/no-recursive-context-method-calls.ts b/packages/lint/src/rules/no-recursive-context-method-calls.ts new file mode 100644 index 000000000..37f045948 --- /dev/null +++ b/packages/lint/src/rules/no-recursive-context-method-calls.ts @@ -0,0 +1,2 @@ +const noRecursiveContextMethodCalls: Deno.lint.Rule = {}; +export default noRecursiveContextMethodCalls; diff --git a/packages/lint/src/rules/require-activity-actor.ts b/packages/lint/src/rules/require-activity-actor.ts new file mode 100644 index 000000000..83e05e612 --- /dev/null +++ b/packages/lint/src/rules/require-activity-actor.ts @@ -0,0 +1,2 @@ +const requireActivityActor: Deno.lint.Rule = {}; +export default requireActivityActor; diff --git a/packages/lint/src/rules/require-activity-id.ts b/packages/lint/src/rules/require-activity-id.ts new file mode 100644 index 000000000..7f5390b94 --- /dev/null +++ b/packages/lint/src/rules/require-activity-id.ts @@ -0,0 +1,2 @@ +const requireActivityId: Deno.lint.Rule = {}; +export default requireActivityId; diff --git a/packages/lint/src/rules/require-activity-to.ts b/packages/lint/src/rules/require-activity-to.ts new file mode 100644 index 000000000..9024190fb --- /dev/null +++ b/packages/lint/src/rules/require-activity-to.ts @@ -0,0 +1,2 @@ +const requireActivityTo: Deno.lint.Rule = {}; +export default requireActivityTo; diff --git a/packages/lint/src/rules/require-actor-dispatcher.ts b/packages/lint/src/rules/require-actor-dispatcher.ts new file mode 100644 index 000000000..eb13a296d --- /dev/null +++ b/packages/lint/src/rules/require-actor-dispatcher.ts @@ -0,0 +1,2 @@ +const requireActorDispatcher: Deno.lint.Rule = {}; +export default requireActorDispatcher; diff --git a/packages/lint/src/rules/require-actor-id.ts b/packages/lint/src/rules/require-actor-id.ts new file mode 100644 index 000000000..6a6c3f673 --- /dev/null +++ b/packages/lint/src/rules/require-actor-id.ts @@ -0,0 +1,2 @@ +const requireActorId: Deno.lint.Rule = {}; +export default requireActorId; diff --git a/packages/lint/src/rules/require-actor-return-value.ts b/packages/lint/src/rules/require-actor-return-value.ts new file mode 100644 index 000000000..bfa5e86f1 --- /dev/null +++ b/packages/lint/src/rules/require-actor-return-value.ts @@ -0,0 +1,2 @@ +const requireActorReturnValue: Deno.lint.Rule = {}; +export default requireActorReturnValue; diff --git a/packages/lint/src/rules/require-collection-property-when-dispatcher-set.ts b/packages/lint/src/rules/require-collection-property-when-dispatcher-set.ts new file mode 100644 index 000000000..e6885b0c2 --- /dev/null +++ b/packages/lint/src/rules/require-collection-property-when-dispatcher-set.ts @@ -0,0 +1,2 @@ +const requireCollectionPropertyWhenDispatcherSet: Deno.lint.Rule = {}; +export default requireCollectionPropertyWhenDispatcherSet; diff --git a/packages/lint/src/rules/require-followers-counter.ts b/packages/lint/src/rules/require-followers-counter.ts new file mode 100644 index 000000000..ed678a4e8 --- /dev/null +++ b/packages/lint/src/rules/require-followers-counter.ts @@ -0,0 +1,2 @@ +const requireFollowersCounter: Deno.lint.Rule = {}; +export default requireFollowersCounter; diff --git a/packages/lint/src/rules/require-inbox-listeners.ts b/packages/lint/src/rules/require-inbox-listeners.ts new file mode 100644 index 000000000..d942a76db --- /dev/null +++ b/packages/lint/src/rules/require-inbox-listeners.ts @@ -0,0 +1,2 @@ +const requireInboxListeners: Deno.lint.Rule = {}; +export default requireInboxListeners; diff --git a/packages/lint/src/rules/require-inbox-uri.ts b/packages/lint/src/rules/require-inbox-uri.ts new file mode 100644 index 000000000..b537b7df3 --- /dev/null +++ b/packages/lint/src/rules/require-inbox-uri.ts @@ -0,0 +1,2 @@ +const requireInboxUri: Deno.lint.Rule = {}; +export default requireInboxUri; diff --git a/packages/lint/src/rules/require-integer-timestamp.ts b/packages/lint/src/rules/require-integer-timestamp.ts new file mode 100644 index 000000000..6d2f1d8e3 --- /dev/null +++ b/packages/lint/src/rules/require-integer-timestamp.ts @@ -0,0 +1,2 @@ +const requireIntegerTimestamp: Deno.lint.Rule = {}; +export default requireIntegerTimestamp; diff --git a/packages/lint/src/rules/require-key-public-key.ts b/packages/lint/src/rules/require-key-public-key.ts new file mode 100644 index 000000000..37d2a1161 --- /dev/null +++ b/packages/lint/src/rules/require-key-public-key.ts @@ -0,0 +1,2 @@ +const requireKeyPublicKey: Deno.lint.Rule = {}; +export default requireKeyPublicKey; diff --git a/packages/lint/src/rules/require-matching-actor-id.ts b/packages/lint/src/rules/require-matching-actor-id.ts new file mode 100644 index 000000000..855f30861 --- /dev/null +++ b/packages/lint/src/rules/require-matching-actor-id.ts @@ -0,0 +1,2 @@ +const requireMatchingActorId: Deno.lint.Rule = {}; +export default requireMatchingActorId; diff --git a/packages/lint/src/rules/require-matching-collection-ids.ts b/packages/lint/src/rules/require-matching-collection-ids.ts new file mode 100644 index 000000000..c0e4092cc --- /dev/null +++ b/packages/lint/src/rules/require-matching-collection-ids.ts @@ -0,0 +1,2 @@ +const requireMatchingCollectionIds: Deno.lint.Rule = {}; +export default requireMatchingCollectionIds; diff --git a/packages/lint/src/rules/require-matching-inbox-paths.ts b/packages/lint/src/rules/require-matching-inbox-paths.ts new file mode 100644 index 000000000..97168d094 --- /dev/null +++ b/packages/lint/src/rules/require-matching-inbox-paths.ts @@ -0,0 +1,2 @@ +const requireMatchingInboxPaths: Deno.lint.Rule = {}; +export default requireMatchingInboxPaths; diff --git a/packages/lint/src/rules/require-message-queue-for-inbox.ts b/packages/lint/src/rules/require-message-queue-for-inbox.ts new file mode 100644 index 000000000..8b8e592d3 --- /dev/null +++ b/packages/lint/src/rules/require-message-queue-for-inbox.ts @@ -0,0 +1,2 @@ +const requireMessageQueueForInbox: Deno.lint.Rule = {}; +export default requireMessageQueueForInbox; diff --git a/packages/lint/src/rules/require-pagination-for-collections.ts b/packages/lint/src/rules/require-pagination-for-collections.ts new file mode 100644 index 000000000..0e59e8601 --- /dev/null +++ b/packages/lint/src/rules/require-pagination-for-collections.ts @@ -0,0 +1,2 @@ +const requirePaginationForCollections: Deno.lint.Rule = {}; +export default requirePaginationForCollections; diff --git a/packages/lint/src/rules/require-persistent-kv-store.ts b/packages/lint/src/rules/require-persistent-kv-store.ts new file mode 100644 index 000000000..71ff3340e --- /dev/null +++ b/packages/lint/src/rules/require-persistent-kv-store.ts @@ -0,0 +1,2 @@ +const requirePersistentKvStore: Deno.lint.Rule = {}; +export default requirePersistentKvStore; diff --git a/packages/lint/src/rules/require-signature-fields.ts b/packages/lint/src/rules/require-signature-fields.ts new file mode 100644 index 000000000..6a5df5ba5 --- /dev/null +++ b/packages/lint/src/rules/require-signature-fields.ts @@ -0,0 +1,2 @@ +const requireSignatureFields: Deno.lint.Rule = {}; +export default requireSignatureFields; diff --git a/packages/lint/src/rules/require-signature-verification.ts b/packages/lint/src/rules/require-signature-verification.ts new file mode 100644 index 000000000..4128239e1 --- /dev/null +++ b/packages/lint/src/rules/require-signature-verification.ts @@ -0,0 +1,2 @@ +const requireSignatureVerification: Deno.lint.Rule = {}; +export default requireSignatureVerification; diff --git a/packages/lint/src/rules/require-type-guard-for-activity-listeners.ts b/packages/lint/src/rules/require-type-guard-for-activity-listeners.ts new file mode 100644 index 000000000..41f13ce22 --- /dev/null +++ b/packages/lint/src/rules/require-type-guard-for-activity-listeners.ts @@ -0,0 +1,2 @@ +const requireTypeGuardForActivityListeners: Deno.lint.Rule = {}; +export default requireTypeGuardForActivityListeners; diff --git a/packages/lint/src/rules/require-valid-uri-template-variables.ts b/packages/lint/src/rules/require-valid-uri-template-variables.ts new file mode 100644 index 000000000..c16e72df6 --- /dev/null +++ b/packages/lint/src/rules/require-valid-uri-template-variables.ts @@ -0,0 +1,2 @@ +const requireValidUriTemplateVariables: Deno.lint.Rule = {}; +export default requireValidUriTemplateVariables; From eda4f40f5c467c3431c36534fb7a3611048845ee Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Thu, 6 Nov 2025 10:56:57 +0000 Subject: [PATCH 04/41] Added @fedify/lint to workspace --- deno.json | 1 + pnpm-workspace.yaml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/deno.json b/deno.json index 16911e216..10638ff33 100644 --- a/deno.json +++ b/deno.json @@ -10,6 +10,7 @@ "./packages/h3", "./packages/hono", "./packages/koa", + "./packages/lint", "./packages/postgres", "./packages/redis", "./packages/relay", diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b82a3bc53..91b1bf0f4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,9 +7,9 @@ packages: - packages/fastify - packages/fedify - packages/h3 -- packages/h3 - packages/hono - packages/koa +- packages/lint - packages/nestjs - packages/next - packages/postgres From 655197dc7e9dc270fe4fb4d3b4b2e37909f4a2a9 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Tue, 18 Nov 2025 08:13:16 +0000 Subject: [PATCH 05/41] Fixed rules --- packages/lint/src/mod.ts | 106 ++++++++---------- .../rules/actor-assertion-method-required.ts | 2 + .../rules/actor-featured-property-mismatch.ts | 2 + .../rules/actor-featured-property-required.ts | 2 + .../actor-featured-tags-property-mismatch.ts | 2 + .../actor-featured-tags-property-required.ts | 2 + .../actor-followers-property-mismatch.ts | 2 + .../actor-followers-property-required.ts | 2 + .../actor-following-property-mismatch.ts | 2 + .../actor-following-property-required.ts | 2 + packages/lint/src/rules/actor-id-mismatch.ts | 2 + packages/lint/src/rules/actor-id-required.ts | 2 + .../rules/actor-inbox-property-mismatch.ts | 2 + .../rules/actor-inbox-property-required.ts | 2 + .../rules/actor-liked-property-mismatch.ts | 2 + .../rules/actor-liked-property-required.ts | 2 + .../rules/actor-outbox-property-mismatch.ts | 2 + .../rules/actor-outbox-property-required.ts | 2 + .../src/rules/actor-public-key-required.ts | 2 + .../actor-shared-inbox-property-mismatch.ts | 2 + .../actor-shared-inbox-property-required.ts | 2 + .../collection-filtering-not-implemented.ts | 2 + .../rules/ed25519-key-required-for-proof.ts | 2 + .../rules/no-deprecated-handle-property.ts | 2 - .../rules/no-deprecated-handle-variable.ts | 2 - .../lint/src/rules/no-duplicate-dispatcher.ts | 2 - .../src/rules/no-duplicate-inbox-listeners.ts | 2 - .../rules/no-memory-kv-store-in-production.ts | 2 - .../no-recursive-context-method-calls.ts | 2 - .../lint/src/rules/require-activity-actor.ts | 2 - .../lint/src/rules/require-activity-id.ts | 2 - .../lint/src/rules/require-activity-to.ts | 2 - .../src/rules/require-actor-dispatcher.ts | 2 - packages/lint/src/rules/require-actor-id.ts | 2 - .../src/rules/require-actor-return-value.ts | 2 - ...collection-property-when-dispatcher-set.ts | 2 - .../src/rules/require-followers-counter.ts | 2 - .../lint/src/rules/require-inbox-listeners.ts | 2 - packages/lint/src/rules/require-inbox-uri.ts | 2 - .../src/rules/require-integer-timestamp.ts | 2 - .../lint/src/rules/require-key-public-key.ts | 2 - .../src/rules/require-matching-actor-id.ts | 2 - .../rules/require-matching-collection-ids.ts | 2 - .../src/rules/require-matching-inbox-paths.ts | 2 - .../rules/require-message-queue-for-inbox.ts | 2 - .../require-pagination-for-collections.ts | 2 - .../src/rules/require-persistent-kv-store.ts | 2 - .../src/rules/require-signature-fields.ts | 2 - .../rules/require-signature-verification.ts | 2 - ...quire-type-guard-for-activity-listeners.ts | 2 - .../require-valid-uri-template-variables.ts | 2 - .../rsa-key-required-for-http-signature.ts | 2 + .../rsa-key-required-for-ld-signature.ts | 2 + 53 files changed, 96 insertions(+), 114 deletions(-) create mode 100644 packages/lint/src/rules/actor-assertion-method-required.ts create mode 100644 packages/lint/src/rules/actor-featured-property-mismatch.ts create mode 100644 packages/lint/src/rules/actor-featured-property-required.ts create mode 100644 packages/lint/src/rules/actor-featured-tags-property-mismatch.ts create mode 100644 packages/lint/src/rules/actor-featured-tags-property-required.ts create mode 100644 packages/lint/src/rules/actor-followers-property-mismatch.ts create mode 100644 packages/lint/src/rules/actor-followers-property-required.ts create mode 100644 packages/lint/src/rules/actor-following-property-mismatch.ts create mode 100644 packages/lint/src/rules/actor-following-property-required.ts create mode 100644 packages/lint/src/rules/actor-id-mismatch.ts create mode 100644 packages/lint/src/rules/actor-id-required.ts create mode 100644 packages/lint/src/rules/actor-inbox-property-mismatch.ts create mode 100644 packages/lint/src/rules/actor-inbox-property-required.ts create mode 100644 packages/lint/src/rules/actor-liked-property-mismatch.ts create mode 100644 packages/lint/src/rules/actor-liked-property-required.ts create mode 100644 packages/lint/src/rules/actor-outbox-property-mismatch.ts create mode 100644 packages/lint/src/rules/actor-outbox-property-required.ts create mode 100644 packages/lint/src/rules/actor-public-key-required.ts create mode 100644 packages/lint/src/rules/actor-shared-inbox-property-mismatch.ts create mode 100644 packages/lint/src/rules/actor-shared-inbox-property-required.ts create mode 100644 packages/lint/src/rules/collection-filtering-not-implemented.ts create mode 100644 packages/lint/src/rules/ed25519-key-required-for-proof.ts delete mode 100644 packages/lint/src/rules/no-deprecated-handle-property.ts delete mode 100644 packages/lint/src/rules/no-deprecated-handle-variable.ts delete mode 100644 packages/lint/src/rules/no-duplicate-dispatcher.ts delete mode 100644 packages/lint/src/rules/no-duplicate-inbox-listeners.ts delete mode 100644 packages/lint/src/rules/no-memory-kv-store-in-production.ts delete mode 100644 packages/lint/src/rules/no-recursive-context-method-calls.ts delete mode 100644 packages/lint/src/rules/require-activity-actor.ts delete mode 100644 packages/lint/src/rules/require-activity-id.ts delete mode 100644 packages/lint/src/rules/require-activity-to.ts delete mode 100644 packages/lint/src/rules/require-actor-dispatcher.ts delete mode 100644 packages/lint/src/rules/require-actor-id.ts delete mode 100644 packages/lint/src/rules/require-actor-return-value.ts delete mode 100644 packages/lint/src/rules/require-collection-property-when-dispatcher-set.ts delete mode 100644 packages/lint/src/rules/require-followers-counter.ts delete mode 100644 packages/lint/src/rules/require-inbox-listeners.ts delete mode 100644 packages/lint/src/rules/require-inbox-uri.ts delete mode 100644 packages/lint/src/rules/require-integer-timestamp.ts delete mode 100644 packages/lint/src/rules/require-key-public-key.ts delete mode 100644 packages/lint/src/rules/require-matching-actor-id.ts delete mode 100644 packages/lint/src/rules/require-matching-collection-ids.ts delete mode 100644 packages/lint/src/rules/require-matching-inbox-paths.ts delete mode 100644 packages/lint/src/rules/require-message-queue-for-inbox.ts delete mode 100644 packages/lint/src/rules/require-pagination-for-collections.ts delete mode 100644 packages/lint/src/rules/require-persistent-kv-store.ts delete mode 100644 packages/lint/src/rules/require-signature-fields.ts delete mode 100644 packages/lint/src/rules/require-signature-verification.ts delete mode 100644 packages/lint/src/rules/require-type-guard-for-activity-listeners.ts delete mode 100644 packages/lint/src/rules/require-valid-uri-template-variables.ts create mode 100644 packages/lint/src/rules/rsa-key-required-for-http-signature.ts create mode 100644 packages/lint/src/rules/rsa-key-required-for-ld-signature.ts diff --git a/packages/lint/src/mod.ts b/packages/lint/src/mod.ts index c692024a8..88b4ef561 100644 --- a/packages/lint/src/mod.ts +++ b/packages/lint/src/mod.ts @@ -1,65 +1,55 @@ -import noDeprecatedHandleProperty from "./rules/no-deprecated-handle-property.ts"; -import noDeprecatedHandleVariable from "./rules/no-deprecated-handle-variable.ts"; -import noDuplicateDispatcher from "./rules/no-duplicate-dispatcher.ts"; -import noDuplicateInboxListeners from "./rules/no-duplicate-inbox-listeners.ts"; -import noMemoryKvStoreInProduction from "./rules/no-memory-kv-store-in-production.ts"; -import noRecursiveContextMethodCalls from "./rules/no-recursive-context-method-calls.ts"; -import requireActivityActor from "./rules/require-activity-actor.ts"; -import requireActivityId from "./rules/require-activity-id.ts"; -import requireActivityTo from "./rules/require-activity-to.ts"; -import requireActorDispatcher from "./rules/require-actor-dispatcher.ts"; -import requireActorId from "./rules/require-actor-id.ts"; -import requireActorReturnValue from "./rules/require-actor-return-value.ts"; -import requireCollectionPropertyWhenDispatcherSet from "./rules/require-collection-property-when-dispatcher-set.ts"; -import requireFollowersCounter from "./rules/require-followers-counter.ts"; -import requireInboxListeners from "./rules/require-inbox-listeners.ts"; -import requireInboxUri from "./rules/require-inbox-uri.ts"; -import requireIntegerTimestamp from "./rules/require-integer-timestamp.ts"; -import requireKeyPublicKey from "./rules/require-key-public-key.ts"; -import requireMatchingActorId from "./rules/require-matching-actor-id.ts"; -import requireMatchingCollectionIds from "./rules/require-matching-collection-ids.ts"; -import requireMatchingInboxPaths from "./rules/require-matching-inbox-paths.ts"; -import requireMessageQueueForInbox from "./rules/require-message-queue-for-inbox.ts"; -import requirePaginationForCollections from "./rules/require-pagination-for-collections.ts"; -import requirePersistentKvStore from "./rules/require-persistent-kv-store.ts"; -import requireSignatureFields from "./rules/require-signature-fields.ts"; -import requireSignatureVerification from "./rules/require-signature-verification.ts"; -import requireTypeGuardForActivityListeners from "./rules/require-type-guard-for-activity-listeners.ts"; -import requireValidUriTemplateVariables from "./rules/require-valid-uri-template-variables.ts"; +import actorAssertionMethodRequired from "./rules/actor-assertion-method-required.ts"; +import actorFeaturedPropertyMismatch from "./rules/actor-featured-property-mismatch.ts"; +import actorFeaturedPropertyRequired from "./rules/actor-featured-property-required.ts"; +import actorFeaturedTagsPropertyMismatch from "./rules/actor-featured-tags-property-mismatch.ts"; +import actorFeaturedTagsPropertyRequired from "./rules/actor-featured-tags-property-required.ts"; +import actorFollowersPropertyMismatch from "./rules/actor-followers-property-mismatch.ts"; +import actorFollowersPropertyRequired from "./rules/actor-followers-property-required.ts"; +import actorFollowingPropertyMismatch from "./rules/actor-following-property-mismatch.ts"; +import actorFollowingPropertyRequired from "./rules/actor-following-property-required.ts"; +import actorIdMismatch from "./rules/actor-id-mismatch.ts"; +import actorIdRequired from "./rules/actor-id-required.ts"; +import actorInboxPropertyMismatch from "./rules/actor-inbox-property-mismatch.ts"; +import actorInboxPropertyRequired from "./rules/actor-inbox-property-required.ts"; +import actorLikedPropertyMismatch from "./rules/actor-liked-property-mismatch.ts"; +import actorLikedPropertyRequired from "./rules/actor-liked-property-required.ts"; +import actorOutboxPropertyMismatch from "./rules/actor-outbox-property-mismatch.ts"; +import actorOutboxPropertyRequired from "./rules/actor-outbox-property-required.ts"; +import actorPublicKeyRequired from "./rules/actor-public-key-required.ts"; +import actorSharedInboxPropertyMismatch from "./rules/actor-shared-inbox-property-mismatch.ts"; +import actorSharedInboxPropertyRequired from "./rules/actor-shared-inbox-property-required.ts"; +import collectionFilteringNotImplemented from "./rules/collection-filtering-not-implemented.ts"; +import ed25519KeyRequiredForProof from "./rules/ed25519-key-required-for-proof.ts"; +import rsaKeyRequiredForHttpSignature from "./rules/rsa-key-required-for-http-signature.ts"; +import rsaKeyRequiredForLdSignature from "./rules/rsa-key-required-for-ld-signature.ts"; const plugin: Deno.lint.Plugin = { name: "@fedify/lint", rules: { - "require-actor-dispatcher": requireActorDispatcher, - "require-inbox-listeners": requireInboxListeners, - "require-signature-verification": requireSignatureVerification, - "require-integer-timestamp": requireIntegerTimestamp, - "require-signature-fields": requireSignatureFields, - "require-key-public-key": requireKeyPublicKey, - "require-matching-actor-id": requireMatchingActorId, - "require-matching-collection-ids": requireMatchingCollectionIds, - "no-deprecated-handle-variable": noDeprecatedHandleVariable, - "no-deprecated-handle-property": noDeprecatedHandleProperty, - "no-duplicate-dispatcher": noDuplicateDispatcher, - "no-duplicate-inbox-listeners": noDuplicateInboxListeners, - "require-valid-uri-template-variables": requireValidUriTemplateVariables, - "require-matching-inbox-paths": requireMatchingInboxPaths, - "require-actor-return-value": requireActorReturnValue, - "no-recursive-context-method-calls": noRecursiveContextMethodCalls, - "require-type-guard-for-activity-listeners": - requireTypeGuardForActivityListeners, - "require-collection-property-when-dispatcher-set": - requireCollectionPropertyWhenDispatcherSet, - "require-actor-id": requireActorId, - "require-activity-actor": requireActivityActor, - "require-activity-id": requireActivityId, - "require-activity-to": requireActivityTo, - "require-inbox-uri": requireInboxUri, - "no-memory-kv-store-in-production": noMemoryKvStoreInProduction, - "require-message-queue-for-inbox": requireMessageQueueForInbox, - "require-persistent-kv-store": requirePersistentKvStore, - "require-pagination-for-collections": requirePaginationForCollections, - "require-followers-counter": requireFollowersCounter, + "actor-id-required": actorIdRequired, + "actor-id-mismatch": actorIdMismatch, + "actor-following-property-required": actorFollowingPropertyRequired, + "actor-following-property-mismatch": actorFollowingPropertyMismatch, + "actor-followers-property-required": actorFollowersPropertyRequired, + "actor-followers-property-mismatch": actorFollowersPropertyMismatch, + "actor-outbox-property-required": actorOutboxPropertyRequired, + "actor-outbox-property-mismatch": actorOutboxPropertyMismatch, + "actor-liked-property-required": actorLikedPropertyRequired, + "actor-liked-property-mismatch": actorLikedPropertyMismatch, + "actor-featured-property-required": actorFeaturedPropertyRequired, + "actor-featured-property-mismatch": actorFeaturedPropertyMismatch, + "actor-featured-tags-property-required": actorFeaturedTagsPropertyRequired, + "actor-featured-tags-property-mismatch": actorFeaturedTagsPropertyMismatch, + "collection-filtering-not-implemented": collectionFilteringNotImplemented, + "actor-inbox-property-required": actorInboxPropertyRequired, + "actor-inbox-property-mismatch": actorInboxPropertyMismatch, + "actor-shared-inbox-property-required": actorSharedInboxPropertyRequired, + "actor-shared-inbox-property-mismatch": actorSharedInboxPropertyMismatch, + "actor-public-key-required": actorPublicKeyRequired, + "actor-assertion-method-required": actorAssertionMethodRequired, + "rsa-key-required-for-http-signature": rsaKeyRequiredForHttpSignature, + "rsa-key-required-for-ld-signature": rsaKeyRequiredForLdSignature, + "ed25519-key-required-for-proof": ed25519KeyRequiredForProof, }, }; diff --git a/packages/lint/src/rules/actor-assertion-method-required.ts b/packages/lint/src/rules/actor-assertion-method-required.ts new file mode 100644 index 000000000..dde5540a5 --- /dev/null +++ b/packages/lint/src/rules/actor-assertion-method-required.ts @@ -0,0 +1,2 @@ +const actorAssertionMethodRequired: Deno.lint.Rule = {}; +export default actorAssertionMethodRequired; diff --git a/packages/lint/src/rules/actor-featured-property-mismatch.ts b/packages/lint/src/rules/actor-featured-property-mismatch.ts new file mode 100644 index 000000000..d644a1966 --- /dev/null +++ b/packages/lint/src/rules/actor-featured-property-mismatch.ts @@ -0,0 +1,2 @@ +const actorFeaturedPropertyMismatch: Deno.lint.Rule = {}; +export default actorFeaturedPropertyMismatch; diff --git a/packages/lint/src/rules/actor-featured-property-required.ts b/packages/lint/src/rules/actor-featured-property-required.ts new file mode 100644 index 000000000..7bed738a4 --- /dev/null +++ b/packages/lint/src/rules/actor-featured-property-required.ts @@ -0,0 +1,2 @@ +const actorFeaturedPropertyRequired: Deno.lint.Rule = {}; +export default actorFeaturedPropertyRequired; diff --git a/packages/lint/src/rules/actor-featured-tags-property-mismatch.ts b/packages/lint/src/rules/actor-featured-tags-property-mismatch.ts new file mode 100644 index 000000000..78e61889c --- /dev/null +++ b/packages/lint/src/rules/actor-featured-tags-property-mismatch.ts @@ -0,0 +1,2 @@ +const actorFeaturedTagsPropertyMismatch: Deno.lint.Rule = {}; +export default actorFeaturedTagsPropertyMismatch; diff --git a/packages/lint/src/rules/actor-featured-tags-property-required.ts b/packages/lint/src/rules/actor-featured-tags-property-required.ts new file mode 100644 index 000000000..8d8e7a83e --- /dev/null +++ b/packages/lint/src/rules/actor-featured-tags-property-required.ts @@ -0,0 +1,2 @@ +const actorFeaturedTagsPropertyRequired: Deno.lint.Rule = {}; +export default actorFeaturedTagsPropertyRequired; diff --git a/packages/lint/src/rules/actor-followers-property-mismatch.ts b/packages/lint/src/rules/actor-followers-property-mismatch.ts new file mode 100644 index 000000000..e1142d3a2 --- /dev/null +++ b/packages/lint/src/rules/actor-followers-property-mismatch.ts @@ -0,0 +1,2 @@ +const actorFollowersPropertyMismatch: Deno.lint.Rule = {}; +export default actorFollowersPropertyMismatch; diff --git a/packages/lint/src/rules/actor-followers-property-required.ts b/packages/lint/src/rules/actor-followers-property-required.ts new file mode 100644 index 000000000..0cc54f820 --- /dev/null +++ b/packages/lint/src/rules/actor-followers-property-required.ts @@ -0,0 +1,2 @@ +const actorFollowersPropertyRequired: Deno.lint.Rule = {}; +export default actorFollowersPropertyRequired; diff --git a/packages/lint/src/rules/actor-following-property-mismatch.ts b/packages/lint/src/rules/actor-following-property-mismatch.ts new file mode 100644 index 000000000..c78dddcc6 --- /dev/null +++ b/packages/lint/src/rules/actor-following-property-mismatch.ts @@ -0,0 +1,2 @@ +const actorFollowingPropertyMismatch: Deno.lint.Rule = {}; +export default actorFollowingPropertyMismatch; diff --git a/packages/lint/src/rules/actor-following-property-required.ts b/packages/lint/src/rules/actor-following-property-required.ts new file mode 100644 index 000000000..978a6a4cf --- /dev/null +++ b/packages/lint/src/rules/actor-following-property-required.ts @@ -0,0 +1,2 @@ +const actorFollowingPropertyRequired: Deno.lint.Rule = {}; +export default actorFollowingPropertyRequired; diff --git a/packages/lint/src/rules/actor-id-mismatch.ts b/packages/lint/src/rules/actor-id-mismatch.ts new file mode 100644 index 000000000..d60d64ab1 --- /dev/null +++ b/packages/lint/src/rules/actor-id-mismatch.ts @@ -0,0 +1,2 @@ +const actorIdMismatch: Deno.lint.Rule = {}; +export default actorIdMismatch; diff --git a/packages/lint/src/rules/actor-id-required.ts b/packages/lint/src/rules/actor-id-required.ts new file mode 100644 index 000000000..e3fa99bc3 --- /dev/null +++ b/packages/lint/src/rules/actor-id-required.ts @@ -0,0 +1,2 @@ +const actorIdRequired: Deno.lint.Rule = {}; +export default actorIdRequired; diff --git a/packages/lint/src/rules/actor-inbox-property-mismatch.ts b/packages/lint/src/rules/actor-inbox-property-mismatch.ts new file mode 100644 index 000000000..3c95b44d0 --- /dev/null +++ b/packages/lint/src/rules/actor-inbox-property-mismatch.ts @@ -0,0 +1,2 @@ +const actorInboxPropertyMismatch: Deno.lint.Rule = {}; +export default actorInboxPropertyMismatch; diff --git a/packages/lint/src/rules/actor-inbox-property-required.ts b/packages/lint/src/rules/actor-inbox-property-required.ts new file mode 100644 index 000000000..0135a9308 --- /dev/null +++ b/packages/lint/src/rules/actor-inbox-property-required.ts @@ -0,0 +1,2 @@ +const actorInboxPropertyRequired: Deno.lint.Rule = {}; +export default actorInboxPropertyRequired; diff --git a/packages/lint/src/rules/actor-liked-property-mismatch.ts b/packages/lint/src/rules/actor-liked-property-mismatch.ts new file mode 100644 index 000000000..3e13af942 --- /dev/null +++ b/packages/lint/src/rules/actor-liked-property-mismatch.ts @@ -0,0 +1,2 @@ +const actorLikedPropertyMismatch: Deno.lint.Rule = {}; +export default actorLikedPropertyMismatch; diff --git a/packages/lint/src/rules/actor-liked-property-required.ts b/packages/lint/src/rules/actor-liked-property-required.ts new file mode 100644 index 000000000..c402618c5 --- /dev/null +++ b/packages/lint/src/rules/actor-liked-property-required.ts @@ -0,0 +1,2 @@ +const actorLikedPropertyRequired: Deno.lint.Rule = {}; +export default actorLikedPropertyRequired; diff --git a/packages/lint/src/rules/actor-outbox-property-mismatch.ts b/packages/lint/src/rules/actor-outbox-property-mismatch.ts new file mode 100644 index 000000000..749e64109 --- /dev/null +++ b/packages/lint/src/rules/actor-outbox-property-mismatch.ts @@ -0,0 +1,2 @@ +const actorOutboxPropertyMismatch: Deno.lint.Rule = {}; +export default actorOutboxPropertyMismatch; diff --git a/packages/lint/src/rules/actor-outbox-property-required.ts b/packages/lint/src/rules/actor-outbox-property-required.ts new file mode 100644 index 000000000..81012f7fc --- /dev/null +++ b/packages/lint/src/rules/actor-outbox-property-required.ts @@ -0,0 +1,2 @@ +const actorOutboxPropertyRequired: Deno.lint.Rule = {}; +export default actorOutboxPropertyRequired; diff --git a/packages/lint/src/rules/actor-public-key-required.ts b/packages/lint/src/rules/actor-public-key-required.ts new file mode 100644 index 000000000..c41b7a105 --- /dev/null +++ b/packages/lint/src/rules/actor-public-key-required.ts @@ -0,0 +1,2 @@ +const actorPublicKeyRequired: Deno.lint.Rule = {}; +export default actorPublicKeyRequired; diff --git a/packages/lint/src/rules/actor-shared-inbox-property-mismatch.ts b/packages/lint/src/rules/actor-shared-inbox-property-mismatch.ts new file mode 100644 index 000000000..a858916bf --- /dev/null +++ b/packages/lint/src/rules/actor-shared-inbox-property-mismatch.ts @@ -0,0 +1,2 @@ +const actorSharedInboxPropertyMismatch: Deno.lint.Rule = {}; +export default actorSharedInboxPropertyMismatch; diff --git a/packages/lint/src/rules/actor-shared-inbox-property-required.ts b/packages/lint/src/rules/actor-shared-inbox-property-required.ts new file mode 100644 index 000000000..1b6b58a58 --- /dev/null +++ b/packages/lint/src/rules/actor-shared-inbox-property-required.ts @@ -0,0 +1,2 @@ +const actorSharedInboxPropertyRequired: Deno.lint.Rule = {}; +export default actorSharedInboxPropertyRequired; diff --git a/packages/lint/src/rules/collection-filtering-not-implemented.ts b/packages/lint/src/rules/collection-filtering-not-implemented.ts new file mode 100644 index 000000000..36d230c25 --- /dev/null +++ b/packages/lint/src/rules/collection-filtering-not-implemented.ts @@ -0,0 +1,2 @@ +const collectionFilteringNotImplemented: Deno.lint.Rule = {}; +export default collectionFilteringNotImplemented; diff --git a/packages/lint/src/rules/ed25519-key-required-for-proof.ts b/packages/lint/src/rules/ed25519-key-required-for-proof.ts new file mode 100644 index 000000000..40a233832 --- /dev/null +++ b/packages/lint/src/rules/ed25519-key-required-for-proof.ts @@ -0,0 +1,2 @@ +const ed25519KeyRequiredForProof: Deno.lint.Rule = {}; +export default ed25519KeyRequiredForProof; diff --git a/packages/lint/src/rules/no-deprecated-handle-property.ts b/packages/lint/src/rules/no-deprecated-handle-property.ts deleted file mode 100644 index f106a1d05..000000000 --- a/packages/lint/src/rules/no-deprecated-handle-property.ts +++ /dev/null @@ -1,2 +0,0 @@ -const noDeprecatedHandleProperty: Deno.lint.Rule = {}; -export default noDeprecatedHandleProperty; diff --git a/packages/lint/src/rules/no-deprecated-handle-variable.ts b/packages/lint/src/rules/no-deprecated-handle-variable.ts deleted file mode 100644 index 820bc448a..000000000 --- a/packages/lint/src/rules/no-deprecated-handle-variable.ts +++ /dev/null @@ -1,2 +0,0 @@ -const noDeprecatedHandleVariable: Deno.lint.Rule = {}; -export default noDeprecatedHandleVariable; diff --git a/packages/lint/src/rules/no-duplicate-dispatcher.ts b/packages/lint/src/rules/no-duplicate-dispatcher.ts deleted file mode 100644 index f66408dfe..000000000 --- a/packages/lint/src/rules/no-duplicate-dispatcher.ts +++ /dev/null @@ -1,2 +0,0 @@ -const noDuplicateDispatcher: Deno.lint.Rule = {}; -export default noDuplicateDispatcher; diff --git a/packages/lint/src/rules/no-duplicate-inbox-listeners.ts b/packages/lint/src/rules/no-duplicate-inbox-listeners.ts deleted file mode 100644 index f6301f2af..000000000 --- a/packages/lint/src/rules/no-duplicate-inbox-listeners.ts +++ /dev/null @@ -1,2 +0,0 @@ -const noDuplicateInboxListeners: Deno.lint.Rule = {}; -export default noDuplicateInboxListeners; diff --git a/packages/lint/src/rules/no-memory-kv-store-in-production.ts b/packages/lint/src/rules/no-memory-kv-store-in-production.ts deleted file mode 100644 index 8aabb4484..000000000 --- a/packages/lint/src/rules/no-memory-kv-store-in-production.ts +++ /dev/null @@ -1,2 +0,0 @@ -const noMemoryKvStoreInProduction: Deno.lint.Rule = {}; -export default noMemoryKvStoreInProduction; diff --git a/packages/lint/src/rules/no-recursive-context-method-calls.ts b/packages/lint/src/rules/no-recursive-context-method-calls.ts deleted file mode 100644 index 37f045948..000000000 --- a/packages/lint/src/rules/no-recursive-context-method-calls.ts +++ /dev/null @@ -1,2 +0,0 @@ -const noRecursiveContextMethodCalls: Deno.lint.Rule = {}; -export default noRecursiveContextMethodCalls; diff --git a/packages/lint/src/rules/require-activity-actor.ts b/packages/lint/src/rules/require-activity-actor.ts deleted file mode 100644 index 83e05e612..000000000 --- a/packages/lint/src/rules/require-activity-actor.ts +++ /dev/null @@ -1,2 +0,0 @@ -const requireActivityActor: Deno.lint.Rule = {}; -export default requireActivityActor; diff --git a/packages/lint/src/rules/require-activity-id.ts b/packages/lint/src/rules/require-activity-id.ts deleted file mode 100644 index 7f5390b94..000000000 --- a/packages/lint/src/rules/require-activity-id.ts +++ /dev/null @@ -1,2 +0,0 @@ -const requireActivityId: Deno.lint.Rule = {}; -export default requireActivityId; diff --git a/packages/lint/src/rules/require-activity-to.ts b/packages/lint/src/rules/require-activity-to.ts deleted file mode 100644 index 9024190fb..000000000 --- a/packages/lint/src/rules/require-activity-to.ts +++ /dev/null @@ -1,2 +0,0 @@ -const requireActivityTo: Deno.lint.Rule = {}; -export default requireActivityTo; diff --git a/packages/lint/src/rules/require-actor-dispatcher.ts b/packages/lint/src/rules/require-actor-dispatcher.ts deleted file mode 100644 index eb13a296d..000000000 --- a/packages/lint/src/rules/require-actor-dispatcher.ts +++ /dev/null @@ -1,2 +0,0 @@ -const requireActorDispatcher: Deno.lint.Rule = {}; -export default requireActorDispatcher; diff --git a/packages/lint/src/rules/require-actor-id.ts b/packages/lint/src/rules/require-actor-id.ts deleted file mode 100644 index 6a6c3f673..000000000 --- a/packages/lint/src/rules/require-actor-id.ts +++ /dev/null @@ -1,2 +0,0 @@ -const requireActorId: Deno.lint.Rule = {}; -export default requireActorId; diff --git a/packages/lint/src/rules/require-actor-return-value.ts b/packages/lint/src/rules/require-actor-return-value.ts deleted file mode 100644 index bfa5e86f1..000000000 --- a/packages/lint/src/rules/require-actor-return-value.ts +++ /dev/null @@ -1,2 +0,0 @@ -const requireActorReturnValue: Deno.lint.Rule = {}; -export default requireActorReturnValue; diff --git a/packages/lint/src/rules/require-collection-property-when-dispatcher-set.ts b/packages/lint/src/rules/require-collection-property-when-dispatcher-set.ts deleted file mode 100644 index e6885b0c2..000000000 --- a/packages/lint/src/rules/require-collection-property-when-dispatcher-set.ts +++ /dev/null @@ -1,2 +0,0 @@ -const requireCollectionPropertyWhenDispatcherSet: Deno.lint.Rule = {}; -export default requireCollectionPropertyWhenDispatcherSet; diff --git a/packages/lint/src/rules/require-followers-counter.ts b/packages/lint/src/rules/require-followers-counter.ts deleted file mode 100644 index ed678a4e8..000000000 --- a/packages/lint/src/rules/require-followers-counter.ts +++ /dev/null @@ -1,2 +0,0 @@ -const requireFollowersCounter: Deno.lint.Rule = {}; -export default requireFollowersCounter; diff --git a/packages/lint/src/rules/require-inbox-listeners.ts b/packages/lint/src/rules/require-inbox-listeners.ts deleted file mode 100644 index d942a76db..000000000 --- a/packages/lint/src/rules/require-inbox-listeners.ts +++ /dev/null @@ -1,2 +0,0 @@ -const requireInboxListeners: Deno.lint.Rule = {}; -export default requireInboxListeners; diff --git a/packages/lint/src/rules/require-inbox-uri.ts b/packages/lint/src/rules/require-inbox-uri.ts deleted file mode 100644 index b537b7df3..000000000 --- a/packages/lint/src/rules/require-inbox-uri.ts +++ /dev/null @@ -1,2 +0,0 @@ -const requireInboxUri: Deno.lint.Rule = {}; -export default requireInboxUri; diff --git a/packages/lint/src/rules/require-integer-timestamp.ts b/packages/lint/src/rules/require-integer-timestamp.ts deleted file mode 100644 index 6d2f1d8e3..000000000 --- a/packages/lint/src/rules/require-integer-timestamp.ts +++ /dev/null @@ -1,2 +0,0 @@ -const requireIntegerTimestamp: Deno.lint.Rule = {}; -export default requireIntegerTimestamp; diff --git a/packages/lint/src/rules/require-key-public-key.ts b/packages/lint/src/rules/require-key-public-key.ts deleted file mode 100644 index 37d2a1161..000000000 --- a/packages/lint/src/rules/require-key-public-key.ts +++ /dev/null @@ -1,2 +0,0 @@ -const requireKeyPublicKey: Deno.lint.Rule = {}; -export default requireKeyPublicKey; diff --git a/packages/lint/src/rules/require-matching-actor-id.ts b/packages/lint/src/rules/require-matching-actor-id.ts deleted file mode 100644 index 855f30861..000000000 --- a/packages/lint/src/rules/require-matching-actor-id.ts +++ /dev/null @@ -1,2 +0,0 @@ -const requireMatchingActorId: Deno.lint.Rule = {}; -export default requireMatchingActorId; diff --git a/packages/lint/src/rules/require-matching-collection-ids.ts b/packages/lint/src/rules/require-matching-collection-ids.ts deleted file mode 100644 index c0e4092cc..000000000 --- a/packages/lint/src/rules/require-matching-collection-ids.ts +++ /dev/null @@ -1,2 +0,0 @@ -const requireMatchingCollectionIds: Deno.lint.Rule = {}; -export default requireMatchingCollectionIds; diff --git a/packages/lint/src/rules/require-matching-inbox-paths.ts b/packages/lint/src/rules/require-matching-inbox-paths.ts deleted file mode 100644 index 97168d094..000000000 --- a/packages/lint/src/rules/require-matching-inbox-paths.ts +++ /dev/null @@ -1,2 +0,0 @@ -const requireMatchingInboxPaths: Deno.lint.Rule = {}; -export default requireMatchingInboxPaths; diff --git a/packages/lint/src/rules/require-message-queue-for-inbox.ts b/packages/lint/src/rules/require-message-queue-for-inbox.ts deleted file mode 100644 index 8b8e592d3..000000000 --- a/packages/lint/src/rules/require-message-queue-for-inbox.ts +++ /dev/null @@ -1,2 +0,0 @@ -const requireMessageQueueForInbox: Deno.lint.Rule = {}; -export default requireMessageQueueForInbox; diff --git a/packages/lint/src/rules/require-pagination-for-collections.ts b/packages/lint/src/rules/require-pagination-for-collections.ts deleted file mode 100644 index 0e59e8601..000000000 --- a/packages/lint/src/rules/require-pagination-for-collections.ts +++ /dev/null @@ -1,2 +0,0 @@ -const requirePaginationForCollections: Deno.lint.Rule = {}; -export default requirePaginationForCollections; diff --git a/packages/lint/src/rules/require-persistent-kv-store.ts b/packages/lint/src/rules/require-persistent-kv-store.ts deleted file mode 100644 index 71ff3340e..000000000 --- a/packages/lint/src/rules/require-persistent-kv-store.ts +++ /dev/null @@ -1,2 +0,0 @@ -const requirePersistentKvStore: Deno.lint.Rule = {}; -export default requirePersistentKvStore; diff --git a/packages/lint/src/rules/require-signature-fields.ts b/packages/lint/src/rules/require-signature-fields.ts deleted file mode 100644 index 6a5df5ba5..000000000 --- a/packages/lint/src/rules/require-signature-fields.ts +++ /dev/null @@ -1,2 +0,0 @@ -const requireSignatureFields: Deno.lint.Rule = {}; -export default requireSignatureFields; diff --git a/packages/lint/src/rules/require-signature-verification.ts b/packages/lint/src/rules/require-signature-verification.ts deleted file mode 100644 index 4128239e1..000000000 --- a/packages/lint/src/rules/require-signature-verification.ts +++ /dev/null @@ -1,2 +0,0 @@ -const requireSignatureVerification: Deno.lint.Rule = {}; -export default requireSignatureVerification; diff --git a/packages/lint/src/rules/require-type-guard-for-activity-listeners.ts b/packages/lint/src/rules/require-type-guard-for-activity-listeners.ts deleted file mode 100644 index 41f13ce22..000000000 --- a/packages/lint/src/rules/require-type-guard-for-activity-listeners.ts +++ /dev/null @@ -1,2 +0,0 @@ -const requireTypeGuardForActivityListeners: Deno.lint.Rule = {}; -export default requireTypeGuardForActivityListeners; diff --git a/packages/lint/src/rules/require-valid-uri-template-variables.ts b/packages/lint/src/rules/require-valid-uri-template-variables.ts deleted file mode 100644 index c16e72df6..000000000 --- a/packages/lint/src/rules/require-valid-uri-template-variables.ts +++ /dev/null @@ -1,2 +0,0 @@ -const requireValidUriTemplateVariables: Deno.lint.Rule = {}; -export default requireValidUriTemplateVariables; diff --git a/packages/lint/src/rules/rsa-key-required-for-http-signature.ts b/packages/lint/src/rules/rsa-key-required-for-http-signature.ts new file mode 100644 index 000000000..bafec2302 --- /dev/null +++ b/packages/lint/src/rules/rsa-key-required-for-http-signature.ts @@ -0,0 +1,2 @@ +const rsaKeyRequiredForHttpSignature: Deno.lint.Rule = {}; +export default rsaKeyRequiredForHttpSignature; diff --git a/packages/lint/src/rules/rsa-key-required-for-ld-signature.ts b/packages/lint/src/rules/rsa-key-required-for-ld-signature.ts new file mode 100644 index 000000000..771826031 --- /dev/null +++ b/packages/lint/src/rules/rsa-key-required-for-ld-signature.ts @@ -0,0 +1,2 @@ +const rsaKeyRequiredForLdSignature: Deno.lint.Rule = {}; +export default rsaKeyRequiredForLdSignature; From c5b7a94cc521778daeb696ad729c7d1e0feba491 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Tue, 18 Nov 2025 08:57:13 +0000 Subject: [PATCH 06/41] Fixed package name --- packages/lint/deno.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lint/deno.json b/packages/lint/deno.json index 888e85b0f..d6507d570 100644 --- a/packages/lint/deno.json +++ b/packages/lint/deno.json @@ -1,5 +1,5 @@ { - "name": "@fedify/fedify", + "name": "@fedify/lint", "version": "2.0.0", "license": "MIT", "exports": { From 199a0e2ce41532715f09653c1cabe39c27f77ca1 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Wed, 19 Nov 2025 03:13:41 +0000 Subject: [PATCH 07/41] Added @fxts/core in workspace --- deno.json | 1 + deno.lock | 870 ++++++++++++++------------------------------ pnpm-lock.yaml | 24 +- pnpm-workspace.yaml | 2 +- 4 files changed, 292 insertions(+), 605 deletions(-) diff --git a/deno.json b/deno.json index 10638ff33..e89c2cb4a 100644 --- a/deno.json +++ b/deno.json @@ -24,6 +24,7 @@ "imports": { "@cloudflare/workers-types": "npm:@cloudflare/workers-types@^4.20250529.0", "@david/dax": "jsr:@david/dax@^0.43.2", + "@fxts/core": "npm:@fxts/core@^1.20.0", "@js-temporal/polyfill": "npm:@js-temporal/polyfill@^0.5.1", "@logtape/file": "jsr:@logtape/file@^1.2.2", "@logtape/logtape": "jsr:@logtape/logtape@^1.2.2", diff --git a/deno.lock b/deno.lock index 3c6ae2c75..0c38af8e4 100644 --- a/deno.lock +++ b/deno.lock @@ -5,14 +5,16 @@ "jsr:@david/dax@~0.43.2": "0.43.2", "jsr:@david/path@0.2": "0.2.0", "jsr:@david/which@~0.4.1": "0.4.1", - "jsr:@es-toolkit/es-toolkit@^1.39.5": "1.39.10", + "jsr:@es-toolkit/es-toolkit@^1.39.5": "1.43.0", "jsr:@hongminhee/localtunnel@0.3": "0.3.0", - "jsr:@hono/hono@^4.8.3": "4.10.2", - "jsr:@logtape/file@^1.2.2": "1.2.2", - "jsr:@logtape/logtape@^1.0.4": "1.2.2", - "jsr:@logtape/logtape@^1.2.2": "1.2.2", - "jsr:@optique/core@~0.6.1": "0.6.1", - "jsr:@optique/run@~0.6.1": "0.6.1", + "jsr:@hono/hono@^4.8.3": "4.11.1", + "jsr:@logtape/file@^1.2.2": "1.3.5", + "jsr:@logtape/logtape@^1.0.4": "1.3.5", + "jsr:@logtape/logtape@^1.2.2": "1.3.5", + "jsr:@logtape/logtape@^1.3.5": "1.3.5", + "jsr:@optique/core@~0.6.1": "0.6.5", + "jsr:@optique/core@~0.6.5": "0.6.5", + "jsr:@optique/run@~0.6.1": "0.6.5", "jsr:@std/bytes@^1.0.5": "1.0.6", "jsr:@std/fmt@1": "1.0.8", "jsr:@std/fs@1": "1.0.20", @@ -21,13 +23,13 @@ "jsr:@std/path@1": "1.1.3", "jsr:@std/path@^1.0.6": "1.1.3", "jsr:@std/path@^1.1.3": "1.1.3", - "jsr:@std/yaml@^1.0.8": "1.0.10", "npm:@alinea/suite@~0.6.3": "0.6.3", "npm:@cfworker/json-schema@^4.1.1": "4.1.1", - "npm:@cloudflare/workers-types@^4.20250529.0": "4.20251213.0", + "npm:@cloudflare/workers-types@^4.20250529.0": "4.20251219.0", "npm:@fxts/core@^1.15.0": "1.21.1", + "npm:@fxts/core@^1.20.0": "1.21.1", "npm:@hongminhee/localtunnel@0.3": "0.3.0", - "npm:@inquirer/prompts@^7.8.4": "7.10.1_@types+node@22.19.2", + "npm:@inquirer/prompts@^7.8.4": "7.10.1_@types+node@22.19.3", "npm:@jimp/core@^1.6.0": "1.6.0", "npm:@jimp/wasm-webp@^1.6.0": "1.6.0", "npm:@js-temporal/polyfill@~0.5.1": "0.5.1", @@ -40,10 +42,10 @@ "npm:@optique/core@~0.6.1": "0.6.5", "npm:@optique/run@~0.6.1": "0.6.5", "npm:@poppanator/http-constants@^1.1.1": "1.1.1", - "npm:@sveltejs/kit@2": "2.49.2_@opentelemetry+api@1.9.0_@sveltejs+vite-plugin-svelte@6.2.1__svelte@5.46.0___acorn@8.15.0__vite@7.2.7___@types+node@22.19.2___tsx@4.21.0___yaml@2.8.2___picomatch@4.0.3__@types+node@22.19.2__tsx@4.21.0__yaml@2.8.2_svelte@5.46.0__acorn@8.15.0_vite@7.2.7__@types+node@22.19.2__tsx@4.21.0__yaml@2.8.2__picomatch@4.0.3_acorn@8.15.0_@types+node@22.19.2_tsx@4.21.0_yaml@2.8.2", + "npm:@sveltejs/kit@2": "2.49.2_@opentelemetry+api@1.9.0_@sveltejs+vite-plugin-svelte@6.2.1__svelte@5.46.0___acorn@8.15.0__vite@7.3.0___@types+node@22.19.3___tsx@4.21.0___yaml@2.8.2___picomatch@4.0.3__@types+node@22.19.3__tsx@4.21.0__yaml@2.8.2_svelte@5.46.0__acorn@8.15.0_vite@7.3.0__@types+node@22.19.3__tsx@4.21.0__yaml@2.8.2__picomatch@4.0.3_acorn@8.15.0_@types+node@22.19.3_tsx@4.21.0_yaml@2.8.2", "npm:@types/amqplib@~0.10.7": "0.10.8", - "npm:@types/node@^22.16.0": "22.19.2", - "npm:@types/node@^24.2.1": "24.10.3", + "npm:@types/node@^22.16.0": "22.19.3", + "npm:@types/node@^24.2.1": "24.10.4", "npm:amqplib@~0.10.8": "0.10.9", "npm:asn1js@^3.0.5": "3.0.7", "npm:asn1js@^3.0.6": "3.0.7", @@ -64,17 +66,17 @@ "npm:fetch-mock@^12.5.2": "12.6.0", "npm:fetch-mock@^12.5.4": "12.6.0", "npm:h3@^1.15.0": "1.15.4", - "npm:hono@^4.8.3": "4.11.0", + "npm:hono@^4.8.3": "4.11.1", "npm:icojs@~0.19.5": "0.19.5", "npm:inquirer-toggle@^1.0.1": "1.0.1", - "npm:inquirer@^12.9.4": "12.11.1_@types+node@22.19.2", + "npm:inquirer@^12.9.4": "12.11.1_@types+node@22.19.3", "npm:ioredis@^5.6.1": "5.8.2", "npm:jimp@^1.6.0": "1.6.0", "npm:json-canon@^1.0.1": "1.0.1", "npm:json-preserve-indent@^1.1.3": "1.1.3", "npm:jsonld@9": "9.0.0", "npm:koa@2": "2.16.3", - "npm:miniflare@^4.20250523.0": "4.20251210.0", + "npm:miniflare@^4.20250523.0": "4.20251217.0", "npm:multicodec@^3.2.1": "3.2.1", "npm:ora@^8.2.0": "8.2.0", "npm:pkijs@^3.2.4": "3.3.3", @@ -84,12 +86,12 @@ "npm:shiki@^1.6.4": "1.29.2", "npm:srvx@~0.8.7": "0.8.16", "npm:structured-field-values@^2.0.4": "2.0.4", - "npm:tsdown@~0.12.9": "0.12.9_rolldown@1.0.0-beta.54", + "npm:tsdown@~0.12.9": "0.12.9_rolldown@1.0.0-beta.55", "npm:tsx@^4.19.4": "4.21.0", "npm:uri-template-router@1": "1.0.0", "npm:url-template@^3.1.1": "3.1.1", "npm:urlpattern-polyfill@^10.1.0": "10.1.0", - "npm:wrangler@^4.17.0": "4.54.0_@cloudflare+workers-types@4.20251213.0_unenv@2.0.0-rc.24_workerd@1.20251210.0", + "npm:wrangler@^4.17.0": "4.56.0_@cloudflare+workers-types@4.20251219.0_unenv@2.0.0-rc.24_workerd@1.20251217.0", "npm:yaml@^2.8.1": "2.8.2" }, "jsr": { @@ -118,8 +120,8 @@ "@david/which@0.4.1": { "integrity": "896a682b111f92ab866cc70c5b4afab2f5899d2f9bde31ed00203b9c250f225e" }, - "@es-toolkit/es-toolkit@1.39.10": { - "integrity": "8757072a13aa64b3b349ba2b9d7d22fbe7ea6f138506c6cd2222d767cd79918f" + "@es-toolkit/es-toolkit@1.43.0": { + "integrity": "ce62274cd14ad9317290b5d77e2c2bf752b78a96e393c0306640f59753e7e043" }, "@hongminhee/localtunnel@0.3.0": { "integrity": "4ad858acd963b5fad45b188d54cf751ac8fbe0aae495e1d3ce607e3730270ff4", @@ -127,25 +129,25 @@ "jsr:@logtape/logtape@^1.0.4" ] }, - "@hono/hono@4.10.2": { - "integrity": "9cdb32df3b1f8eba43b197035a90e0c3c543ddf99f75cebfa31a2414bc5633f8" + "@hono/hono@4.11.1": { + "integrity": "28bb28c7322a1379e7132af05ef76aef09f86bb8cdd7077c67e004dea288ac0d" }, - "@logtape/file@1.2.2": { - "integrity": "a602f49148d0d5553dd3398506e9579e7294bab5706dff91f4c18abde53f1985", + "@logtape/file@1.3.5": { + "integrity": "6e5248e873e260109267b79bd3fb19f307a664a2233c5ae6f699d697549db985", "dependencies": [ - "jsr:@logtape/logtape@^1.2.2" + "jsr:@logtape/logtape@^1.3.5" ] }, - "@logtape/logtape@1.2.2": { - "integrity": "628fa8d52245aa1c81fd3af91622b621c1974fc8be71afb7574cd256b6aff953" + "@logtape/logtape@1.3.5": { + "integrity": "a5cdb130daf1a9d384006b0f850cc4443bfc2e163dadc6fa667875e79770beb3" }, - "@optique/core@0.6.1": { - "integrity": "87fe16d06724b8d83c114c7c734780426c34ff865627cda9ee21cd18ada198af" + "@optique/core@0.6.5": { + "integrity": "6568b8aef8b576e1b9ad8d57d5abdfe4dfeb960953205a1b9dced1426f7e0109" }, - "@optique/run@0.6.1": { - "integrity": "5e4017221e22dde1e731a81ccae7c0cf5d40e1f392b78193fe5ccc6475fb88b4", + "@optique/run@0.6.5": { + "integrity": "1c6ab2606ea4c5d2f6b851a857e95a37634797223e82aa8bc14af42988fc8fa9", "dependencies": [ - "jsr:@optique/core@~0.6.1" + "jsr:@optique/core@~0.6.5" ] }, "@std/bytes@1.0.6": { @@ -175,9 +177,6 @@ "dependencies": [ "jsr:@std/internal" ] - }, - "@std/yaml@1.0.10": { - "integrity": "245706ea3511cc50c8c6d00339c23ea2ffa27bd2c7ea5445338f8feff31fa58e" } }, "npm": { @@ -229,7 +228,7 @@ "mime@3.0.0" ] }, - "@cloudflare/unenv-preset@2.7.13_unenv@2.0.0-rc.24_workerd@1.20251210.0": { + "@cloudflare/unenv-preset@2.7.13_unenv@2.0.0-rc.24_workerd@1.20251217.0": { "integrity": "sha512-NulO1H8R/DzsJguLC0ndMuk4Ufv0KSlN+E54ay9rn9ZCQo0kpAPwwh3LhgpZ96a3Dr6L9LqW57M4CqC34iLOvw==", "dependencies": [ "unenv", @@ -239,33 +238,33 @@ "workerd" ] }, - "@cloudflare/workerd-darwin-64@1.20251210.0": { - "integrity": "sha512-Nn9X1moUDERA9xtFdCQ2XpQXgAS9pOjiCxvOT8sVx9UJLAiBLkfSCGbpsYdarODGybXCpjRlc77Yppuolvt7oQ==", + "@cloudflare/workerd-darwin-64@1.20251217.0": { + "integrity": "sha512-DN6vT+9ho61d/1/YuILW4VS+N1JBLaixWRL1vqNmhgbf8J8VHwWWotrRruEUYigJKx2yZyw6YsasE+yLXgx/Fw==", "os": ["darwin"], "cpu": ["x64"] }, - "@cloudflare/workerd-darwin-arm64@1.20251210.0": { - "integrity": "sha512-Mg8iYIZQFnbevq/ls9eW/eneWTk/EE13Pej1MwfkY5et0jVpdHnvOLywy/o+QtMJFef1AjsqXGULwAneYyBfHw==", + "@cloudflare/workerd-darwin-arm64@1.20251217.0": { + "integrity": "sha512-5nZOpRTkHmtcTc4Wbr1mj/O3dLb6aHZSiJuVBgtdbVcVmOXueSay3hnw1PXEyR+vpTKGUPkM+omUIslKHWnXDw==", "os": ["darwin"], "cpu": ["arm64"] }, - "@cloudflare/workerd-linux-64@1.20251210.0": { - "integrity": "sha512-kjC2fCZhZ2Gkm1biwk2qByAYpGguK5Gf5ic8owzSCUw0FOUfQxTZUT9Lp3gApxsfTLbbnLBrX/xzWjywH9QR4g==", + "@cloudflare/workerd-linux-64@1.20251217.0": { + "integrity": "sha512-uoPGhMaZVXPpCsU0oG3HQzyVpXCGi5rU+jcHRjUI7DXM4EwctBGvZ380Knkja36qtl+ZvSKVR1pUFSGdK+45Pg==", "os": ["linux"], "cpu": ["x64"] }, - "@cloudflare/workerd-linux-arm64@1.20251210.0": { - "integrity": "sha512-2IB37nXi7PZVQLa1OCuO7/6pNxqisRSO8DmCQ5x/3sezI5op1vwOxAcb1osAnuVsVN9bbvpw70HJvhKruFJTuA==", + "@cloudflare/workerd-linux-arm64@1.20251217.0": { + "integrity": "sha512-ixHnHKsiz1Xko+eDgCJOZ7EEUZKtmnYq3AjW3nkVcLFypSLks4C29E45zVewdaN4wq8sCLeyQCl6r1kS17+DQQ==", "os": ["linux"], "cpu": ["arm64"] }, - "@cloudflare/workerd-windows-64@1.20251210.0": { - "integrity": "sha512-Uaz6/9XE+D6E7pCY4OvkCuJHu7HcSDzeGcCGY1HLhojXhHd7yL52c3yfiyJdS8hPatiAa0nn5qSI/42+aTdDSw==", + "@cloudflare/workerd-windows-64@1.20251217.0": { + "integrity": "sha512-rP6USX+7ctynz3AtmKi+EvlLP3Xdr1ETrSdcnv693/I5QdUwBxq4yE1Lj6CV7GJizX6opXKYg8QMq0Q4eB9zRQ==", "os": ["win32"], "cpu": ["x64"] }, - "@cloudflare/workers-types@4.20251213.0": { - "integrity": "sha512-PJAGdKfU7hs39C2YOFNLTdrfdqG6rbaVj5UuI306zS+TPokiskRLEgUXKqS6avN9Uu9Nyuf2a0hqoumLQCnJlQ==" + "@cloudflare/workers-types@4.20251219.0": { + "integrity": "sha512-qwuvc3ZDdCfcK9dJrBSFHOsX8kL72sypfBilzEWbbb+slB2NiggjsHeGMV2ZQiQc1zyBMQPjIvsVeE7Apxp7hw==" }, "@colors/colors@1.5.0": { "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==" @@ -302,396 +301,136 @@ "tslib" ] }, - "@esbuild/aix-ppc64@0.25.12": { - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "os": ["aix"], - "cpu": ["ppc64"] - }, "@esbuild/aix-ppc64@0.27.0": { "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", "os": ["aix"], "cpu": ["ppc64"] }, - "@esbuild/aix-ppc64@0.27.1": { - "integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==", - "os": ["aix"], - "cpu": ["ppc64"] - }, - "@esbuild/android-arm64@0.25.12": { - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "os": ["android"], - "cpu": ["arm64"] - }, "@esbuild/android-arm64@0.27.0": { "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", "os": ["android"], "cpu": ["arm64"] }, - "@esbuild/android-arm64@0.27.1": { - "integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==", - "os": ["android"], - "cpu": ["arm64"] - }, - "@esbuild/android-arm@0.25.12": { - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "os": ["android"], - "cpu": ["arm"] - }, "@esbuild/android-arm@0.27.0": { "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", "os": ["android"], "cpu": ["arm"] }, - "@esbuild/android-arm@0.27.1": { - "integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==", - "os": ["android"], - "cpu": ["arm"] - }, - "@esbuild/android-x64@0.25.12": { - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "os": ["android"], - "cpu": ["x64"] - }, "@esbuild/android-x64@0.27.0": { "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", "os": ["android"], "cpu": ["x64"] }, - "@esbuild/android-x64@0.27.1": { - "integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==", - "os": ["android"], - "cpu": ["x64"] - }, - "@esbuild/darwin-arm64@0.25.12": { - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "os": ["darwin"], - "cpu": ["arm64"] - }, "@esbuild/darwin-arm64@0.27.0": { "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", "os": ["darwin"], "cpu": ["arm64"] }, - "@esbuild/darwin-arm64@0.27.1": { - "integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==", - "os": ["darwin"], - "cpu": ["arm64"] - }, - "@esbuild/darwin-x64@0.25.12": { - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "os": ["darwin"], - "cpu": ["x64"] - }, "@esbuild/darwin-x64@0.27.0": { "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", "os": ["darwin"], "cpu": ["x64"] }, - "@esbuild/darwin-x64@0.27.1": { - "integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==", - "os": ["darwin"], - "cpu": ["x64"] - }, - "@esbuild/freebsd-arm64@0.25.12": { - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "os": ["freebsd"], - "cpu": ["arm64"] - }, "@esbuild/freebsd-arm64@0.27.0": { "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", "os": ["freebsd"], "cpu": ["arm64"] }, - "@esbuild/freebsd-arm64@0.27.1": { - "integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==", - "os": ["freebsd"], - "cpu": ["arm64"] - }, - "@esbuild/freebsd-x64@0.25.12": { - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "os": ["freebsd"], - "cpu": ["x64"] - }, "@esbuild/freebsd-x64@0.27.0": { "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", "os": ["freebsd"], "cpu": ["x64"] }, - "@esbuild/freebsd-x64@0.27.1": { - "integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==", - "os": ["freebsd"], - "cpu": ["x64"] - }, - "@esbuild/linux-arm64@0.25.12": { - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "os": ["linux"], - "cpu": ["arm64"] - }, "@esbuild/linux-arm64@0.27.0": { "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", "os": ["linux"], "cpu": ["arm64"] }, - "@esbuild/linux-arm64@0.27.1": { - "integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==", - "os": ["linux"], - "cpu": ["arm64"] - }, - "@esbuild/linux-arm@0.25.12": { - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "os": ["linux"], - "cpu": ["arm"] - }, "@esbuild/linux-arm@0.27.0": { "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", "os": ["linux"], "cpu": ["arm"] }, - "@esbuild/linux-arm@0.27.1": { - "integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==", - "os": ["linux"], - "cpu": ["arm"] - }, - "@esbuild/linux-ia32@0.25.12": { - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "os": ["linux"], - "cpu": ["ia32"] - }, "@esbuild/linux-ia32@0.27.0": { "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", "os": ["linux"], "cpu": ["ia32"] }, - "@esbuild/linux-ia32@0.27.1": { - "integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==", - "os": ["linux"], - "cpu": ["ia32"] - }, - "@esbuild/linux-loong64@0.25.12": { - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "os": ["linux"], - "cpu": ["loong64"] - }, "@esbuild/linux-loong64@0.27.0": { "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", "os": ["linux"], "cpu": ["loong64"] }, - "@esbuild/linux-loong64@0.27.1": { - "integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==", - "os": ["linux"], - "cpu": ["loong64"] - }, - "@esbuild/linux-mips64el@0.25.12": { - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "os": ["linux"], - "cpu": ["mips64el"] - }, "@esbuild/linux-mips64el@0.27.0": { "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", "os": ["linux"], "cpu": ["mips64el"] }, - "@esbuild/linux-mips64el@0.27.1": { - "integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==", - "os": ["linux"], - "cpu": ["mips64el"] - }, - "@esbuild/linux-ppc64@0.25.12": { - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "os": ["linux"], - "cpu": ["ppc64"] - }, "@esbuild/linux-ppc64@0.27.0": { "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", "os": ["linux"], "cpu": ["ppc64"] }, - "@esbuild/linux-ppc64@0.27.1": { - "integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==", - "os": ["linux"], - "cpu": ["ppc64"] - }, - "@esbuild/linux-riscv64@0.25.12": { - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "os": ["linux"], - "cpu": ["riscv64"] - }, "@esbuild/linux-riscv64@0.27.0": { "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", "os": ["linux"], "cpu": ["riscv64"] }, - "@esbuild/linux-riscv64@0.27.1": { - "integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==", - "os": ["linux"], - "cpu": ["riscv64"] - }, - "@esbuild/linux-s390x@0.25.12": { - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "os": ["linux"], - "cpu": ["s390x"] - }, "@esbuild/linux-s390x@0.27.0": { "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", "os": ["linux"], "cpu": ["s390x"] }, - "@esbuild/linux-s390x@0.27.1": { - "integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==", - "os": ["linux"], - "cpu": ["s390x"] - }, - "@esbuild/linux-x64@0.25.12": { - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "os": ["linux"], - "cpu": ["x64"] - }, "@esbuild/linux-x64@0.27.0": { "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", "os": ["linux"], "cpu": ["x64"] }, - "@esbuild/linux-x64@0.27.1": { - "integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==", - "os": ["linux"], - "cpu": ["x64"] - }, - "@esbuild/netbsd-arm64@0.25.12": { - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "os": ["netbsd"], - "cpu": ["arm64"] - }, "@esbuild/netbsd-arm64@0.27.0": { "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", "os": ["netbsd"], "cpu": ["arm64"] }, - "@esbuild/netbsd-arm64@0.27.1": { - "integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==", - "os": ["netbsd"], - "cpu": ["arm64"] - }, - "@esbuild/netbsd-x64@0.25.12": { - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "os": ["netbsd"], - "cpu": ["x64"] - }, "@esbuild/netbsd-x64@0.27.0": { "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", "os": ["netbsd"], "cpu": ["x64"] }, - "@esbuild/netbsd-x64@0.27.1": { - "integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==", - "os": ["netbsd"], - "cpu": ["x64"] - }, - "@esbuild/openbsd-arm64@0.25.12": { - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "os": ["openbsd"], - "cpu": ["arm64"] - }, "@esbuild/openbsd-arm64@0.27.0": { "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", "os": ["openbsd"], "cpu": ["arm64"] }, - "@esbuild/openbsd-arm64@0.27.1": { - "integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==", - "os": ["openbsd"], - "cpu": ["arm64"] - }, - "@esbuild/openbsd-x64@0.25.12": { - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "os": ["openbsd"], - "cpu": ["x64"] - }, "@esbuild/openbsd-x64@0.27.0": { "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", "os": ["openbsd"], "cpu": ["x64"] }, - "@esbuild/openbsd-x64@0.27.1": { - "integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==", - "os": ["openbsd"], - "cpu": ["x64"] - }, - "@esbuild/openharmony-arm64@0.25.12": { - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "os": ["openharmony"], - "cpu": ["arm64"] - }, "@esbuild/openharmony-arm64@0.27.0": { "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", "os": ["openharmony"], "cpu": ["arm64"] }, - "@esbuild/openharmony-arm64@0.27.1": { - "integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==", - "os": ["openharmony"], - "cpu": ["arm64"] - }, - "@esbuild/sunos-x64@0.25.12": { - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "os": ["sunos"], - "cpu": ["x64"] - }, "@esbuild/sunos-x64@0.27.0": { "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", "os": ["sunos"], "cpu": ["x64"] }, - "@esbuild/sunos-x64@0.27.1": { - "integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==", - "os": ["sunos"], - "cpu": ["x64"] - }, - "@esbuild/win32-arm64@0.25.12": { - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "os": ["win32"], - "cpu": ["arm64"] - }, "@esbuild/win32-arm64@0.27.0": { "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", "os": ["win32"], "cpu": ["arm64"] }, - "@esbuild/win32-arm64@0.27.1": { - "integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==", - "os": ["win32"], - "cpu": ["arm64"] - }, - "@esbuild/win32-ia32@0.25.12": { - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "os": ["win32"], - "cpu": ["ia32"] - }, "@esbuild/win32-ia32@0.27.0": { "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", "os": ["win32"], "cpu": ["ia32"] }, - "@esbuild/win32-ia32@0.27.1": { - "integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==", - "os": ["win32"], - "cpu": ["ia32"] - }, - "@esbuild/win32-x64@0.25.12": { - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "os": ["win32"], - "cpu": ["x64"] - }, "@esbuild/win32-x64@0.27.0": { "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", "os": ["win32"], "cpu": ["x64"] }, - "@esbuild/win32-x64@0.27.1": { - "integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==", - "os": ["win32"], - "cpu": ["x64"] - }, "@fastify/ajv-compiler@4.0.5_ajv@8.17.1": { "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==", "dependencies": [ @@ -861,38 +600,38 @@ "@inquirer/ansi@1.0.2": { "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==" }, - "@inquirer/checkbox@4.3.2_@types+node@22.19.2": { + "@inquirer/checkbox@4.3.2_@types+node@22.19.3": { "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", "dependencies": [ "@inquirer/ansi", - "@inquirer/core@10.3.2_@types+node@22.19.2", + "@inquirer/core@10.3.2_@types+node@22.19.3", "@inquirer/figures", - "@inquirer/type@3.0.10_@types+node@22.19.2", - "@types/node@22.19.2", + "@inquirer/type@3.0.10_@types+node@22.19.3", + "@types/node@22.19.3", "yoctocolors-cjs" ], "optionalPeers": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ] }, - "@inquirer/confirm@5.1.21_@types+node@22.19.2": { + "@inquirer/confirm@5.1.21_@types+node@22.19.3": { "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", "dependencies": [ - "@inquirer/core@10.3.2_@types+node@22.19.2", - "@inquirer/type@3.0.10_@types+node@22.19.2", - "@types/node@22.19.2" + "@inquirer/core@10.3.2_@types+node@22.19.3", + "@inquirer/type@3.0.10_@types+node@22.19.3", + "@types/node@22.19.3" ], "optionalPeers": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ] }, - "@inquirer/core@10.3.2_@types+node@22.19.2": { + "@inquirer/core@10.3.2_@types+node@22.19.3": { "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", "dependencies": [ "@inquirer/ansi", "@inquirer/figures", - "@inquirer/type@3.0.10_@types+node@22.19.2", - "@types/node@22.19.2", + "@inquirer/type@3.0.10_@types+node@22.19.3", + "@types/node@22.19.3", "cli-width", "mute-stream@2.0.0", "signal-exit", @@ -900,7 +639,7 @@ "yoctocolors-cjs" ], "optionalPeers": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ] }, "@inquirer/core@8.2.4": { @@ -909,7 +648,7 @@ "@inquirer/figures", "@inquirer/type@1.5.5", "@types/mute-stream", - "@types/node@20.19.26", + "@types/node@20.19.27", "@types/wrap-ansi", "ansi-escapes", "cli-spinners", @@ -921,79 +660,79 @@ "wrap-ansi@6.2.0" ] }, - "@inquirer/editor@4.2.23_@types+node@22.19.2": { + "@inquirer/editor@4.2.23_@types+node@22.19.3": { "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", "dependencies": [ - "@inquirer/core@10.3.2_@types+node@22.19.2", + "@inquirer/core@10.3.2_@types+node@22.19.3", "@inquirer/external-editor", - "@inquirer/type@3.0.10_@types+node@22.19.2", - "@types/node@22.19.2" + "@inquirer/type@3.0.10_@types+node@22.19.3", + "@types/node@22.19.3" ], "optionalPeers": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ] }, - "@inquirer/expand@4.0.23_@types+node@22.19.2": { + "@inquirer/expand@4.0.23_@types+node@22.19.3": { "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", "dependencies": [ - "@inquirer/core@10.3.2_@types+node@22.19.2", - "@inquirer/type@3.0.10_@types+node@22.19.2", - "@types/node@22.19.2", + "@inquirer/core@10.3.2_@types+node@22.19.3", + "@inquirer/type@3.0.10_@types+node@22.19.3", + "@types/node@22.19.3", "yoctocolors-cjs" ], "optionalPeers": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ] }, - "@inquirer/external-editor@1.0.3_@types+node@22.19.2": { + "@inquirer/external-editor@1.0.3_@types+node@22.19.3": { "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", "dependencies": [ - "@types/node@22.19.2", + "@types/node@22.19.3", "chardet", "iconv-lite@0.7.1" ], "optionalPeers": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ] }, "@inquirer/figures@1.0.15": { "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==" }, - "@inquirer/input@4.3.1_@types+node@22.19.2": { + "@inquirer/input@4.3.1_@types+node@22.19.3": { "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", "dependencies": [ - "@inquirer/core@10.3.2_@types+node@22.19.2", - "@inquirer/type@3.0.10_@types+node@22.19.2", - "@types/node@22.19.2" + "@inquirer/core@10.3.2_@types+node@22.19.3", + "@inquirer/type@3.0.10_@types+node@22.19.3", + "@types/node@22.19.3" ], "optionalPeers": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ] }, - "@inquirer/number@3.0.23_@types+node@22.19.2": { + "@inquirer/number@3.0.23_@types+node@22.19.3": { "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", "dependencies": [ - "@inquirer/core@10.3.2_@types+node@22.19.2", - "@inquirer/type@3.0.10_@types+node@22.19.2", - "@types/node@22.19.2" + "@inquirer/core@10.3.2_@types+node@22.19.3", + "@inquirer/type@3.0.10_@types+node@22.19.3", + "@types/node@22.19.3" ], "optionalPeers": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ] }, - "@inquirer/password@4.0.23_@types+node@22.19.2": { + "@inquirer/password@4.0.23_@types+node@22.19.3": { "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", "dependencies": [ "@inquirer/ansi", - "@inquirer/core@10.3.2_@types+node@22.19.2", - "@inquirer/type@3.0.10_@types+node@22.19.2", - "@types/node@22.19.2" + "@inquirer/core@10.3.2_@types+node@22.19.3", + "@inquirer/type@3.0.10_@types+node@22.19.3", + "@types/node@22.19.3" ], "optionalPeers": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ] }, - "@inquirer/prompts@7.10.1_@types+node@22.19.2": { + "@inquirer/prompts@7.10.1_@types+node@22.19.3": { "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", "dependencies": [ "@inquirer/checkbox", @@ -1006,49 +745,49 @@ "@inquirer/rawlist", "@inquirer/search", "@inquirer/select", - "@types/node@22.19.2" + "@types/node@22.19.3" ], "optionalPeers": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ] }, - "@inquirer/rawlist@4.1.11_@types+node@22.19.2": { + "@inquirer/rawlist@4.1.11_@types+node@22.19.3": { "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", "dependencies": [ - "@inquirer/core@10.3.2_@types+node@22.19.2", - "@inquirer/type@3.0.10_@types+node@22.19.2", - "@types/node@22.19.2", + "@inquirer/core@10.3.2_@types+node@22.19.3", + "@inquirer/type@3.0.10_@types+node@22.19.3", + "@types/node@22.19.3", "yoctocolors-cjs" ], "optionalPeers": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ] }, - "@inquirer/search@3.2.2_@types+node@22.19.2": { + "@inquirer/search@3.2.2_@types+node@22.19.3": { "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", "dependencies": [ - "@inquirer/core@10.3.2_@types+node@22.19.2", + "@inquirer/core@10.3.2_@types+node@22.19.3", "@inquirer/figures", - "@inquirer/type@3.0.10_@types+node@22.19.2", - "@types/node@22.19.2", + "@inquirer/type@3.0.10_@types+node@22.19.3", + "@types/node@22.19.3", "yoctocolors-cjs" ], "optionalPeers": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ] }, - "@inquirer/select@4.4.2_@types+node@22.19.2": { + "@inquirer/select@4.4.2_@types+node@22.19.3": { "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", "dependencies": [ "@inquirer/ansi", - "@inquirer/core@10.3.2_@types+node@22.19.2", + "@inquirer/core@10.3.2_@types+node@22.19.3", "@inquirer/figures", - "@inquirer/type@3.0.10_@types+node@22.19.2", - "@types/node@22.19.2", + "@inquirer/type@3.0.10_@types+node@22.19.3", + "@types/node@22.19.3", "yoctocolors-cjs" ], "optionalPeers": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ] }, "@inquirer/type@1.5.5": { @@ -1057,13 +796,13 @@ "mute-stream@1.0.0" ] }, - "@inquirer/type@3.0.10_@types+node@22.19.2": { + "@inquirer/type@3.0.10_@types+node@22.19.3": { "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", "dependencies": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ], "optionalPeers": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ] }, "@ioredis/commands@1.4.0": { @@ -1399,8 +1138,8 @@ "wasm-feature-detect" ] }, - "@logtape/logtape@1.2.2": { - "integrity": "sha512-yx4uj7NUXo5pOk2lljtAf7UN9oDBw9xfxSDC4jxdbfnGDGmq4iJhenWBbU9XV8ilm8AUUMYXq2nk+qvz8E21DA==" + "@logtape/logtape@1.3.5": { + "integrity": "sha512-G+MxWB7Tbv/2764519+Cp6rKXUdRbe/GiRwTvlm/Wv/sNsiquRnx9Hzr9eXaIpAYLT4PrBlkthjJ4gmqdSPrFg==" }, "@lukeed/csprng@1.1.0": { "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==" @@ -1470,8 +1209,8 @@ "@optique/core" ] }, - "@oxc-project/types@0.102.0": { - "integrity": "sha512-8Skrw405g+/UJPKWJ1twIk3BIH2nXdiVlVNtYT23AXVwpsd79es4K+KYt06Fbnkc5BaTvk/COT2JuCLYdwnCdA==" + "@oxc-project/types@0.103.0": { + "integrity": "sha512-bkiYX5kaXWwUessFRSoXFkGIQTmc6dLGdxuRTrC+h8PSnIdZyuXHHlLAeTmOue5Br/a0/a7dHH0Gca6eXn9MKg==" }, "@pinojs/redact@0.4.0": { "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==" @@ -1505,183 +1244,183 @@ "quansync" ] }, - "@rolldown/binding-android-arm64@1.0.0-beta.54": { - "integrity": "sha512-zZRx/ur3Fai3fxiEmVp48+6GCBR48PRWJR1X3TTMn9yiq2bBHlYPgBaQtDOYWXv5H3J5dXujeTyGnuoY+kdGCg==", + "@rolldown/binding-android-arm64@1.0.0-beta.55": { + "integrity": "sha512-5cPpHdO+zp+klznZnIHRO1bMHDq5hS9cqXodEKAaa/dQTPDjnE91OwAsy3o1gT2x4QaY8NzdBXAvutYdaw0WeA==", "os": ["android"], "cpu": ["arm64"] }, - "@rolldown/binding-darwin-arm64@1.0.0-beta.54": { - "integrity": "sha512-zMyFEJmbIs91x22HAA/eUvmZHgjX8tGsD3TJ+WC9aY4bCdl3w84H9vMZmChSHAF1dYvGNH4KQDI2IubeZaCYtg==", + "@rolldown/binding-darwin-arm64@1.0.0-beta.55": { + "integrity": "sha512-l0887CGU2SXZr0UJmeEcXSvtDCOhDTTYXuoWbhrEJ58YQhQk24EVhDhHMTyjJb1PBRniUgNc1G0T51eF8z+TWw==", "os": ["darwin"], "cpu": ["arm64"] }, - "@rolldown/binding-darwin-x64@1.0.0-beta.54": { - "integrity": "sha512-Ex7QttdaVnEpmE/zroUT5Qm10e2+Vjd9q0LX9eXm59SitxDODMpC8GI1Rct5RrLf4GLU4DzdXBj6DGzuR+6g6w==", + "@rolldown/binding-darwin-x64@1.0.0-beta.55": { + "integrity": "sha512-d7qP2AVYzN0tYIP4vJ7nmr26xvmlwdkLD/jWIc9Z9dqh5y0UGPigO3m5eHoHq9BNazmwdD9WzDHbQZyXFZjgtA==", "os": ["darwin"], "cpu": ["x64"] }, - "@rolldown/binding-freebsd-x64@1.0.0-beta.54": { - "integrity": "sha512-E1XO10ryM/Vxw3Q1wvs9s2mSpVBfbHtzkbJcdu26qh17ZmVwNWLiIoqEcbkXm028YwkReG4Gd2gCZ3NxgTQ28Q==", + "@rolldown/binding-freebsd-x64@1.0.0-beta.55": { + "integrity": "sha512-j311E4NOB0VMmXHoDDZhrWidUf7L/Sa6bu/+i2cskvHKU40zcUNPSYeD2YiO2MX+hhDFa5bJwhliYfs+bTrSZw==", "os": ["freebsd"], "cpu": ["x64"] }, - "@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.54": { - "integrity": "sha512-oS73Uks8jczQR9pg0Bj718vap/x71exyJ5yuxu4X5V4MhwRQnky7ANSPm6ARUfraxOqt49IBfcMeGnw2rTSqdA==", + "@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.55": { + "integrity": "sha512-lAsaYWhfNTW2A/9O7zCpb5eIJBrFeNEatOS/DDOZ5V/95NHy50g4b/5ViCqchfyFqRb7MKUR18/+xWkIcDkeIw==", "os": ["linux"], "cpu": ["arm"] }, - "@rolldown/binding-linux-arm64-gnu@1.0.0-beta.54": { - "integrity": "sha512-pY8N2X5C+/ZQcy0eRdfOzOP//OFngP1TaIqDjFwfBPws2UNavKS8SpxhPEgUaYIaT0keVBd/TB+eVy9z+CIOtw==", + "@rolldown/binding-linux-arm64-gnu@1.0.0-beta.55": { + "integrity": "sha512-2x6ffiVLZrQv7Xii9+JdtyT1U3bQhKj59K3eRnYlrXsKyjkjfmiDUVx2n+zSyijisUqD62fcegmx2oLLfeTkCA==", "os": ["linux"], "cpu": ["arm64"] }, - "@rolldown/binding-linux-arm64-musl@1.0.0-beta.54": { - "integrity": "sha512-cgTooAFm2MUmFriB7IYaWBNyqrGlRPKG+yaK2rGFl2rcdOcO24urY4p3eyB0ogqsRLvJbIxwjjYiWiIP7Eo1Cw==", + "@rolldown/binding-linux-arm64-musl@1.0.0-beta.55": { + "integrity": "sha512-QbNncvqAXziya5wleI+OJvmceEE15vE4yn4qfbI/hwT/+8ZcqxyfRZOOh62KjisXxp4D0h3JZspycXYejxAU3w==", "os": ["linux"], "cpu": ["arm64"] }, - "@rolldown/binding-linux-x64-gnu@1.0.0-beta.54": { - "integrity": "sha512-nGyLT1Qau0W+kEL44V2jhHmvfS3wyJW08E4WEu2E6NuIy+uChKN1X0aoxzFIDi2owDsYaZYez/98/f268EupIQ==", + "@rolldown/binding-linux-x64-gnu@1.0.0-beta.55": { + "integrity": "sha512-YZCTZZM+rujxwVc6A+QZaNMJXVtmabmFYLG2VGQTKaBfYGvBKUgtbMEttnp/oZ88BMi2DzadBVhOmfQV8SuHhw==", "os": ["linux"], "cpu": ["x64"] }, - "@rolldown/binding-linux-x64-musl@1.0.0-beta.54": { - "integrity": "sha512-KH374P0TUjDXssROT/orvzaWrzGOptD13PTrltgKwbDprJTMknoLiYsOD6Ttz92O2VuAcCtFuJ1xbyFM2Uo/Xg==", + "@rolldown/binding-linux-x64-musl@1.0.0-beta.55": { + "integrity": "sha512-28q9OQ/DDpFh2keS4BVAlc3N65/wiqKbk5K1pgLdu/uWbKa8hgUJofhXxqO+a+Ya2HVTUuYHneWsI2u+eu3N5Q==", "os": ["linux"], "cpu": ["x64"] }, - "@rolldown/binding-openharmony-arm64@1.0.0-beta.54": { - "integrity": "sha512-oMAVO4wbfAbhpBxPsSp8R7ntL2DchpNfO+tGhN8/sI9jsbYwOv78uIW1fTwOBslhjTVFltGJ+l23mubNQcYNaQ==", + "@rolldown/binding-openharmony-arm64@1.0.0-beta.55": { + "integrity": "sha512-LiCA4BjCnm49B+j1lFzUtlC+4ZphBv0d0g5VqrEJua/uyv9Ey1v9tiaMql1C8c0TVSNDUmrkfHQ71vuQC7YfpQ==", "os": ["openharmony"], "cpu": ["arm64"] }, - "@rolldown/binding-wasm32-wasi@1.0.0-beta.54": { - "integrity": "sha512-MYY/FmY+HehHiQkNx04W5oLy/Fqd1hXYqZmmorSDXvAHnxMbSgmdFicKsSYOg/sVGHBMEP1tTn6kV5sWrS45rA==", + "@rolldown/binding-wasm32-wasi@1.0.0-beta.55": { + "integrity": "sha512-nZ76tY7T0Oe8vamz5Cv5CBJvrqeQxwj1WaJ2GxX8Msqs0zsQMMcvoyxOf0glnJlxxgKjtoBxAOxaAU8ERbW6Tg==", "dependencies": [ "@napi-rs/wasm-runtime" ], "cpu": ["wasm32"] }, - "@rolldown/binding-win32-arm64-msvc@1.0.0-beta.54": { - "integrity": "sha512-66o3uKxUmcYskT9exskxs3OVduXf5x0ndlMkYOjSpBgqzhLtkub136yDvZkNT1OkNDET0odSwcU7aWdpnwzAyg==", + "@rolldown/binding-win32-arm64-msvc@1.0.0-beta.55": { + "integrity": "sha512-TFVVfLfhL1G+pWspYAgPK/FSqjiBtRKYX9hixfs508QVEZPQlubYAepHPA7kEa6lZXYj5ntzF87KC6RNhxo+ew==", "os": ["win32"], "cpu": ["arm64"] }, - "@rolldown/binding-win32-x64-msvc@1.0.0-beta.54": { - "integrity": "sha512-FbbbrboChLBXfeEsOfaypBGqzbdJ/CcSA2BPLCggojnIHy58Jo+AXV7HATY8opZk7194rRbokIT8AfPJtZAWtg==", + "@rolldown/binding-win32-x64-msvc@1.0.0-beta.55": { + "integrity": "sha512-j1WBlk0p+ISgLzMIgl0xHp1aBGXenoK2+qWYc/wil2Vse7kVOdFq9aeQ8ahK6/oxX2teQ5+eDvgjdywqTL+daA==", "os": ["win32"], "cpu": ["x64"] }, - "@rolldown/pluginutils@1.0.0-beta.54": { - "integrity": "sha512-AHgcZ+w7RIRZ65ihSQL8YuoKcpD9Scew4sEeP1BBUT9QdTo6KjwHrZZXjID6nL10fhKessCH6OPany2QKwAwTQ==" + "@rolldown/pluginutils@1.0.0-beta.55": { + "integrity": "sha512-vajw/B3qoi7aYnnD4BQ4VoCcXQWnF0roSwE2iynbNxgW4l9mFwtLmLmUhpDdcTBfKyZm1p/T0D13qG94XBLohA==" }, - "@rollup/rollup-android-arm-eabi@4.53.3": { - "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "@rollup/rollup-android-arm-eabi@4.53.5": { + "integrity": "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==", "os": ["android"], "cpu": ["arm"] }, - "@rollup/rollup-android-arm64@4.53.3": { - "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "@rollup/rollup-android-arm64@4.53.5": { + "integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==", "os": ["android"], "cpu": ["arm64"] }, - "@rollup/rollup-darwin-arm64@4.53.3": { - "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "@rollup/rollup-darwin-arm64@4.53.5": { + "integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==", "os": ["darwin"], "cpu": ["arm64"] }, - "@rollup/rollup-darwin-x64@4.53.3": { - "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "@rollup/rollup-darwin-x64@4.53.5": { + "integrity": "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==", "os": ["darwin"], "cpu": ["x64"] }, - "@rollup/rollup-freebsd-arm64@4.53.3": { - "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "@rollup/rollup-freebsd-arm64@4.53.5": { + "integrity": "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==", "os": ["freebsd"], "cpu": ["arm64"] }, - "@rollup/rollup-freebsd-x64@4.53.3": { - "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "@rollup/rollup-freebsd-x64@4.53.5": { + "integrity": "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==", "os": ["freebsd"], "cpu": ["x64"] }, - "@rollup/rollup-linux-arm-gnueabihf@4.53.3": { - "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "@rollup/rollup-linux-arm-gnueabihf@4.53.5": { + "integrity": "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==", "os": ["linux"], "cpu": ["arm"] }, - "@rollup/rollup-linux-arm-musleabihf@4.53.3": { - "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "@rollup/rollup-linux-arm-musleabihf@4.53.5": { + "integrity": "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==", "os": ["linux"], "cpu": ["arm"] }, - "@rollup/rollup-linux-arm64-gnu@4.53.3": { - "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "@rollup/rollup-linux-arm64-gnu@4.53.5": { + "integrity": "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==", "os": ["linux"], "cpu": ["arm64"] }, - "@rollup/rollup-linux-arm64-musl@4.53.3": { - "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "@rollup/rollup-linux-arm64-musl@4.53.5": { + "integrity": "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==", "os": ["linux"], "cpu": ["arm64"] }, - "@rollup/rollup-linux-loong64-gnu@4.53.3": { - "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "@rollup/rollup-linux-loong64-gnu@4.53.5": { + "integrity": "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==", "os": ["linux"], "cpu": ["loong64"] }, - "@rollup/rollup-linux-ppc64-gnu@4.53.3": { - "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "@rollup/rollup-linux-ppc64-gnu@4.53.5": { + "integrity": "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==", "os": ["linux"], "cpu": ["ppc64"] }, - "@rollup/rollup-linux-riscv64-gnu@4.53.3": { - "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "@rollup/rollup-linux-riscv64-gnu@4.53.5": { + "integrity": "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==", "os": ["linux"], "cpu": ["riscv64"] }, - "@rollup/rollup-linux-riscv64-musl@4.53.3": { - "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "@rollup/rollup-linux-riscv64-musl@4.53.5": { + "integrity": "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==", "os": ["linux"], "cpu": ["riscv64"] }, - "@rollup/rollup-linux-s390x-gnu@4.53.3": { - "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "@rollup/rollup-linux-s390x-gnu@4.53.5": { + "integrity": "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==", "os": ["linux"], "cpu": ["s390x"] }, - "@rollup/rollup-linux-x64-gnu@4.53.3": { - "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "@rollup/rollup-linux-x64-gnu@4.53.5": { + "integrity": "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==", "os": ["linux"], "cpu": ["x64"] }, - "@rollup/rollup-linux-x64-musl@4.53.3": { - "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "@rollup/rollup-linux-x64-musl@4.53.5": { + "integrity": "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==", "os": ["linux"], "cpu": ["x64"] }, - "@rollup/rollup-openharmony-arm64@4.53.3": { - "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "@rollup/rollup-openharmony-arm64@4.53.5": { + "integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==", "os": ["openharmony"], "cpu": ["arm64"] }, - "@rollup/rollup-win32-arm64-msvc@4.53.3": { - "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "@rollup/rollup-win32-arm64-msvc@4.53.5": { + "integrity": "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==", "os": ["win32"], "cpu": ["arm64"] }, - "@rollup/rollup-win32-ia32-msvc@4.53.3": { - "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "@rollup/rollup-win32-ia32-msvc@4.53.5": { + "integrity": "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==", "os": ["win32"], "cpu": ["ia32"] }, - "@rollup/rollup-win32-x64-gnu@4.53.3": { - "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "@rollup/rollup-win32-x64-gnu@4.53.5": { + "integrity": "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==", "os": ["win32"], "cpu": ["x64"] }, - "@rollup/rollup-win32-x64-msvc@4.53.3": { - "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "@rollup/rollup-win32-x64-msvc@4.53.5": { + "integrity": "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==", "os": ["win32"], "cpu": ["x64"] }, @@ -1742,8 +1481,8 @@ "@speed-highlight/core@1.2.12": { "integrity": "sha512-uilwrK0Ygyri5dToHYdZSjcvpS2ZwX0w5aSt3GCEN9hrjxWCoeV4Z2DTXuxjwbntaLQIEEAlCeNQss5SoHvAEA==" }, - "@standard-schema/spec@1.0.0": { - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==" + "@standard-schema/spec@1.1.0": { + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==" }, "@sveltejs/acorn-typescript@1.0.8_acorn@8.15.0": { "integrity": "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==", @@ -1751,7 +1490,7 @@ "acorn@8.15.0" ] }, - "@sveltejs/kit@2.49.2_@opentelemetry+api@1.9.0_@sveltejs+vite-plugin-svelte@6.2.1__svelte@5.46.0___acorn@8.15.0__vite@7.2.7___@types+node@22.19.2___tsx@4.21.0___yaml@2.8.2___picomatch@4.0.3__@types+node@22.19.2__tsx@4.21.0__yaml@2.8.2_svelte@5.46.0__acorn@8.15.0_vite@7.2.7__@types+node@22.19.2__tsx@4.21.0__yaml@2.8.2__picomatch@4.0.3_acorn@8.15.0_@types+node@22.19.2_tsx@4.21.0_yaml@2.8.2": { + "@sveltejs/kit@2.49.2_@opentelemetry+api@1.9.0_@sveltejs+vite-plugin-svelte@6.2.1__svelte@5.46.0___acorn@8.15.0__vite@7.3.0___@types+node@22.19.3___tsx@4.21.0___yaml@2.8.2___picomatch@4.0.3__@types+node@22.19.3__tsx@4.21.0__yaml@2.8.2_svelte@5.46.0__acorn@8.15.0_vite@7.3.0__@types+node@22.19.3__tsx@4.21.0__yaml@2.8.2__picomatch@4.0.3_acorn@8.15.0_@types+node@22.19.3_tsx@4.21.0_yaml@2.8.2": { "integrity": "sha512-Vp3zX/qlwerQmHMP6x0Ry1oY7eKKRcOWGc2P59srOp4zcqyn+etJyQpELgOi4+ZSUgteX8Y387NuwruLgGXLUQ==", "dependencies": [ "@opentelemetry/api", @@ -1777,7 +1516,7 @@ ], "bin": true }, - "@sveltejs/vite-plugin-svelte-inspector@5.0.1_@sveltejs+vite-plugin-svelte@6.2.1__svelte@5.46.0___acorn@8.15.0__vite@7.2.7___@types+node@22.19.2___tsx@4.21.0___yaml@2.8.2___picomatch@4.0.3__@types+node@22.19.2__tsx@4.21.0__yaml@2.8.2_svelte@5.46.0__acorn@8.15.0_vite@7.2.7__@types+node@22.19.2__tsx@4.21.0__yaml@2.8.2__picomatch@4.0.3_@types+node@22.19.2_tsx@4.21.0_yaml@2.8.2": { + "@sveltejs/vite-plugin-svelte-inspector@5.0.1_@sveltejs+vite-plugin-svelte@6.2.1__svelte@5.46.0___acorn@8.15.0__vite@7.3.0___@types+node@22.19.3___tsx@4.21.0___yaml@2.8.2___picomatch@4.0.3__@types+node@22.19.3__tsx@4.21.0__yaml@2.8.2_svelte@5.46.0__acorn@8.15.0_vite@7.3.0__@types+node@22.19.3__tsx@4.21.0__yaml@2.8.2__picomatch@4.0.3_@types+node@22.19.3_tsx@4.21.0_yaml@2.8.2": { "integrity": "sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA==", "dependencies": [ "@sveltejs/vite-plugin-svelte", @@ -1786,7 +1525,7 @@ "vite" ] }, - "@sveltejs/vite-plugin-svelte@6.2.1_svelte@5.46.0__acorn@8.15.0_vite@7.2.7__@types+node@22.19.2__tsx@4.21.0__yaml@2.8.2__picomatch@4.0.3_@types+node@22.19.2_tsx@4.21.0_yaml@2.8.2": { + "@sveltejs/vite-plugin-svelte@6.2.1_svelte@5.46.0__acorn@8.15.0_vite@7.3.0__@types+node@22.19.3__tsx@4.21.0__yaml@2.8.2__picomatch@4.0.3_@types+node@22.19.3_tsx@4.21.0_yaml@2.8.2": { "integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==", "dependencies": [ "@sveltejs/vite-plugin-svelte-inspector", @@ -1818,7 +1557,7 @@ "@types/amqplib@0.10.8": { "integrity": "sha512-vtDp8Pk1wsE/AuQ8/Rgtm6KUZYqcnTgNvEHwzCkX8rL7AGsC6zqAfKAAJhUZXFhM/Pp++tbnUHiam/8vVpPztA==", "dependencies": [ - "@types/node@24.10.3" + "@types/node@24.2.0" ] }, "@types/cookie@0.6.0": { @@ -1845,30 +1584,36 @@ "@types/mute-stream@0.0.4": { "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", "dependencies": [ - "@types/node@24.10.3" + "@types/node@24.2.0" ] }, "@types/node@16.9.1": { "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==" }, - "@types/node@20.19.26": { - "integrity": "sha512-0l6cjgF0XnihUpndDhk+nyD3exio3iKaYROSgvh/qSevPXax3L8p5DBRFjbvalnwatGgHEQn2R88y2fA3g4irg==", + "@types/node@20.19.27": { + "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", "dependencies": [ "undici-types@6.21.0" ] }, - "@types/node@22.19.2": { - "integrity": "sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==", + "@types/node@22.19.3": { + "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", "dependencies": [ "undici-types@6.21.0" ] }, - "@types/node@24.10.3": { - "integrity": "sha512-gqkrWUsS8hcm0r44yn7/xZeV1ERva/nLgrLxFRUGb7aoNMIJfZJ3AC261zDQuOAKC7MiXai1WCpYc48jAHoShQ==", + "@types/node@24.10.4": { + "integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==", "dependencies": [ "undici-types@7.16.0" ] }, + "@types/node@24.2.0": { + "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", + "dependencies": [ + "undici-types@7.10.0" + ] + }, "@types/unist@3.0.3": { "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" }, @@ -2356,101 +2101,35 @@ "es-toolkit@1.43.0": { "integrity": "sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==" }, - "esbuild@0.25.12": { - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "optionalDependencies": [ - "@esbuild/aix-ppc64@0.25.12", - "@esbuild/android-arm@0.25.12", - "@esbuild/android-arm64@0.25.12", - "@esbuild/android-x64@0.25.12", - "@esbuild/darwin-arm64@0.25.12", - "@esbuild/darwin-x64@0.25.12", - "@esbuild/freebsd-arm64@0.25.12", - "@esbuild/freebsd-x64@0.25.12", - "@esbuild/linux-arm@0.25.12", - "@esbuild/linux-arm64@0.25.12", - "@esbuild/linux-ia32@0.25.12", - "@esbuild/linux-loong64@0.25.12", - "@esbuild/linux-mips64el@0.25.12", - "@esbuild/linux-ppc64@0.25.12", - "@esbuild/linux-riscv64@0.25.12", - "@esbuild/linux-s390x@0.25.12", - "@esbuild/linux-x64@0.25.12", - "@esbuild/netbsd-arm64@0.25.12", - "@esbuild/netbsd-x64@0.25.12", - "@esbuild/openbsd-arm64@0.25.12", - "@esbuild/openbsd-x64@0.25.12", - "@esbuild/openharmony-arm64@0.25.12", - "@esbuild/sunos-x64@0.25.12", - "@esbuild/win32-arm64@0.25.12", - "@esbuild/win32-ia32@0.25.12", - "@esbuild/win32-x64@0.25.12" - ], - "scripts": true, - "bin": true - }, "esbuild@0.27.0": { "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", "optionalDependencies": [ - "@esbuild/aix-ppc64@0.27.0", - "@esbuild/android-arm@0.27.0", - "@esbuild/android-arm64@0.27.0", - "@esbuild/android-x64@0.27.0", - "@esbuild/darwin-arm64@0.27.0", - "@esbuild/darwin-x64@0.27.0", - "@esbuild/freebsd-arm64@0.27.0", - "@esbuild/freebsd-x64@0.27.0", - "@esbuild/linux-arm@0.27.0", - "@esbuild/linux-arm64@0.27.0", - "@esbuild/linux-ia32@0.27.0", - "@esbuild/linux-loong64@0.27.0", - "@esbuild/linux-mips64el@0.27.0", - "@esbuild/linux-ppc64@0.27.0", - "@esbuild/linux-riscv64@0.27.0", - "@esbuild/linux-s390x@0.27.0", - "@esbuild/linux-x64@0.27.0", - "@esbuild/netbsd-arm64@0.27.0", - "@esbuild/netbsd-x64@0.27.0", - "@esbuild/openbsd-arm64@0.27.0", - "@esbuild/openbsd-x64@0.27.0", - "@esbuild/openharmony-arm64@0.27.0", - "@esbuild/sunos-x64@0.27.0", - "@esbuild/win32-arm64@0.27.0", - "@esbuild/win32-ia32@0.27.0", - "@esbuild/win32-x64@0.27.0" - ], - "scripts": true, - "bin": true - }, - "esbuild@0.27.1": { - "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==", - "optionalDependencies": [ - "@esbuild/aix-ppc64@0.27.1", - "@esbuild/android-arm@0.27.1", - "@esbuild/android-arm64@0.27.1", - "@esbuild/android-x64@0.27.1", - "@esbuild/darwin-arm64@0.27.1", - "@esbuild/darwin-x64@0.27.1", - "@esbuild/freebsd-arm64@0.27.1", - "@esbuild/freebsd-x64@0.27.1", - "@esbuild/linux-arm@0.27.1", - "@esbuild/linux-arm64@0.27.1", - "@esbuild/linux-ia32@0.27.1", - "@esbuild/linux-loong64@0.27.1", - "@esbuild/linux-mips64el@0.27.1", - "@esbuild/linux-ppc64@0.27.1", - "@esbuild/linux-riscv64@0.27.1", - "@esbuild/linux-s390x@0.27.1", - "@esbuild/linux-x64@0.27.1", - "@esbuild/netbsd-arm64@0.27.1", - "@esbuild/netbsd-x64@0.27.1", - "@esbuild/openbsd-arm64@0.27.1", - "@esbuild/openbsd-x64@0.27.1", - "@esbuild/openharmony-arm64@0.27.1", - "@esbuild/sunos-x64@0.27.1", - "@esbuild/win32-arm64@0.27.1", - "@esbuild/win32-ia32@0.27.1", - "@esbuild/win32-x64@0.27.1" + "@esbuild/aix-ppc64", + "@esbuild/android-arm", + "@esbuild/android-arm64", + "@esbuild/android-x64", + "@esbuild/darwin-arm64", + "@esbuild/darwin-x64", + "@esbuild/freebsd-arm64", + "@esbuild/freebsd-x64", + "@esbuild/linux-arm", + "@esbuild/linux-arm64", + "@esbuild/linux-ia32", + "@esbuild/linux-loong64", + "@esbuild/linux-mips64el", + "@esbuild/linux-ppc64", + "@esbuild/linux-riscv64", + "@esbuild/linux-s390x", + "@esbuild/linux-x64", + "@esbuild/netbsd-arm64", + "@esbuild/netbsd-x64", + "@esbuild/openbsd-arm64", + "@esbuild/openbsd-x64", + "@esbuild/openharmony-arm64", + "@esbuild/sunos-x64", + "@esbuild/win32-arm64", + "@esbuild/win32-ia32", + "@esbuild/win32-x64" ], "scripts": true, "bin": true @@ -2777,8 +2456,8 @@ "highlight.js@10.7.3": { "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==" }, - "hono@4.11.0": { - "integrity": "sha512-Jg8uZzN2ul8/qlyid5FO8O624F3AK0wKtkgoeEON1qBum1rM1itYBxoMKu/1SPJC7F1+xlIZsJMmc4HHhJ1AWg==" + "hono@4.11.1": { + "integrity": "sha512-KsFcH0xxHes0J4zaQgWbYwmz3UPOOskdqZmItstUG93+Wk1ePBLkLGwbP9zlmh1BFUiL8Qp+Xfu9P7feJWpGNg==" }, "hookable@5.5.3": { "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==" @@ -2803,16 +2482,6 @@ "toidentifier" ] }, - "http-errors@2.0.0": { - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": [ - "depd@2.0.0", - "inherits", - "setprototypeof", - "statuses@2.0.1", - "toidentifier" - ] - }, "http-errors@2.0.1": { "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "dependencies": [ @@ -2864,20 +2533,20 @@ "@inquirer/core@8.2.4" ] }, - "inquirer@12.11.1_@types+node@22.19.2": { + "inquirer@12.11.1_@types+node@22.19.3": { "integrity": "sha512-9VF7mrY+3OmsAfjH3yKz/pLbJ5z22E23hENKw3/LNSaA/sAt3v49bDRY+Ygct1xwuKT+U+cBfTzjCPySna69Qw==", "dependencies": [ "@inquirer/ansi", - "@inquirer/core@10.3.2_@types+node@22.19.2", + "@inquirer/core@10.3.2_@types+node@22.19.3", "@inquirer/prompts", - "@inquirer/type@3.0.10_@types+node@22.19.2", - "@types/node@22.19.2", + "@inquirer/type@3.0.10_@types+node@22.19.3", + "@types/node@22.19.3", "mute-stream@2.0.0", "run-async", "rxjs" ], "optionalPeers": [ - "@types/node@22.19.2" + "@types/node@22.19.3" ] }, "ioredis@5.8.2": { @@ -3192,8 +2861,8 @@ "mimic-function@5.0.1": { "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==" }, - "miniflare@4.20251210.0": { - "integrity": "sha512-k6kIoXwGVqlPZb0hcn+X7BmnK+8BjIIkusQPY22kCo2RaQJ/LzAjtxHQdGXerlHSnJyQivDQsL6BJHMpQfUFyw==", + "miniflare@4.20251217.0": { + "integrity": "sha512-8xsTQbPS6YV+ABZl9qiJIbsum6hbpbhqiyKpOVdzZrhK+1N8EFpT8R6aBZff7kezGmxYZSntjgjqTwJmj3JLgA==", "dependencies": [ "@cspotcode/source-map-support", "acorn@8.14.0", @@ -3589,7 +3258,7 @@ "rfdc@1.4.1": { "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" }, - "rolldown-plugin-dts@0.13.14_rolldown@1.0.0-beta.54": { + "rolldown-plugin-dts@0.13.14_rolldown@1.0.0-beta.55": { "integrity": "sha512-wjNhHZz9dlN6PTIXyizB6u/mAg1wEFMW9yw7imEVe3CxHSRnNHVyycIX0yDEOVJfDNISLPbkCIPEpFpizy5+PQ==", "dependencies": [ "@babel/generator", @@ -3603,8 +3272,8 @@ "rolldown" ] }, - "rolldown@1.0.0-beta.54": { - "integrity": "sha512-3lIvjCWgjPL3gmiATUdV1NeVBGJZy6FdtwgLPol25tAkn46Q/MsVGfCSNswXwFOxGrxglPaN20IeALSIFuFyEg==", + "rolldown@1.0.0-beta.55": { + "integrity": "sha512-r8Ws43aYCnfO07ao0SvQRz4TBAtZJjGWNvScRBOHuiNHvjfECOJBIqJv0nUkL1GYcltjvvHswRilDF1ocsC0+g==", "dependencies": [ "@oxc-project/types", "@rolldown/pluginutils" @@ -3626,8 +3295,8 @@ ], "bin": true }, - "rollup@4.53.3": { - "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "rollup@4.53.5": { + "integrity": "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==", "dependencies": [ "@types/estree" ], @@ -3706,26 +3375,26 @@ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "bin": true }, - "send@0.19.0": { - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "send@0.19.2": { + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", "dependencies": [ "debug@2.6.9", "depd@2.0.0", "destroy", - "encodeurl@1.0.2", + "encodeurl@2.0.0", "escape-html", "etag", "fresh", - "http-errors@2.0.0", + "http-errors@2.0.1", "mime@1.6.0", "ms@2.1.3", "on-finished", "range-parser", - "statuses@2.0.1" + "statuses@2.0.2" ] }, - "serve-static@1.16.2": { - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "serve-static@1.16.3": { + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", "dependencies": [ "encodeurl@2.0.0", "escape-html", @@ -3866,9 +3535,6 @@ "statuses@1.5.0": { "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==" }, - "statuses@2.0.1": { - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" - }, "statuses@2.0.2": { "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==" }, @@ -4038,7 +3704,7 @@ "trim-lines@3.0.1": { "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==" }, - "tsdown@0.12.9_rolldown@1.0.0-beta.54": { + "tsdown@0.12.9_rolldown@1.0.0-beta.55": { "integrity": "sha512-MfrXm9PIlT3saovtWKf/gCJJ/NQCdE0SiREkdNC+9Qy6UHhdeDPxnkFaBD7xttVUmgp0yUHtGirpoLB+OVLuLA==", "dependencies": [ "ansis", @@ -4066,7 +3732,7 @@ "tsx@4.21.0": { "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dependencies": [ - "esbuild@0.27.1", + "esbuild", "get-tsconfig" ], "optionalDependencies": [ @@ -4125,6 +3791,9 @@ "undici-types@6.21.0": { "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" }, + "undici-types@7.10.0": { + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==" + }, "undici-types@7.16.0": { "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==" }, @@ -4221,11 +3890,11 @@ "vfile-message" ] }, - "vite@7.2.7_@types+node@22.19.2_tsx@4.21.0_yaml@2.8.2_picomatch@4.0.3": { - "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==", + "vite@7.3.0_@types+node@22.19.3_tsx@4.21.0_yaml@2.8.2_picomatch@4.0.3": { + "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dependencies": [ - "@types/node@22.19.2", - "esbuild@0.25.12", + "@types/node@22.19.3", + "esbuild", "fdir", "picomatch", "postcss", @@ -4238,13 +3907,13 @@ "fsevents" ], "optionalPeers": [ - "@types/node@22.19.2", + "@types/node@22.19.3", "tsx", "yaml" ], "bin": true }, - "vitefu@1.1.1_vite@7.2.7__@types+node@22.19.2__tsx@4.21.0__yaml@2.8.2__picomatch@4.0.3_@types+node@22.19.2_tsx@4.21.0_yaml@2.8.2": { + "vitefu@1.1.1_vite@7.3.0__@types+node@22.19.3__tsx@4.21.0__yaml@2.8.2__picomatch@4.0.3_@types+node@22.19.3_tsx@4.21.0_yaml@2.8.2": { "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", "dependencies": [ "vite" @@ -4269,8 +3938,8 @@ "webidl-conversions" ] }, - "workerd@1.20251210.0": { - "integrity": "sha512-9MUUneP1BnRE9XAYi94FXxHmiLGbO75EHQZsgWqSiOXjoXSqJCw8aQbIEPxCy19TclEl/kHUFYce8ST2W+Qpjw==", + "workerd@1.20251217.0": { + "integrity": "sha512-s3mHDSWwHTduyY8kpHOsl27ZJ4ziDBJlc18PfBvNMqNnhO7yBeemlxH7bo7yQyU1foJrIZ6IENHDDg0Z9N8zQA==", "optionalDependencies": [ "@cloudflare/workerd-darwin-64", "@cloudflare/workerd-darwin-arm64", @@ -4281,14 +3950,14 @@ "scripts": true, "bin": true }, - "wrangler@4.54.0_@cloudflare+workers-types@4.20251213.0_unenv@2.0.0-rc.24_workerd@1.20251210.0": { - "integrity": "sha512-bANFsjDwJLbprYoBK+hUDZsVbUv2SqJd8QvArLIcZk+fPq4h/Ohtj5vkKXD3k0s2bD1DXLk08D+hYmeNH+xC6A==", + "wrangler@4.56.0_@cloudflare+workers-types@4.20251219.0_unenv@2.0.0-rc.24_workerd@1.20251217.0": { + "integrity": "sha512-Nqi8duQeRbA+31QrD6QlWHW3IZVnuuRxMy7DEg46deUzywivmaRV/euBN5KKXDPtA24VyhYsK7I0tkb7P5DM2w==", "dependencies": [ "@cloudflare/kv-asset-handler", "@cloudflare/unenv-preset", "@cloudflare/workers-types", "blake3-wasm", - "esbuild@0.27.0", + "esbuild", "miniflare", "path-to-regexp@6.3.0", "unenv", @@ -4408,6 +4077,7 @@ "jsr:@std/testing@0.224", "jsr:@std/yaml@^1.0.8", "npm:@cloudflare/workers-types@^4.20250529.0", + "npm:@fxts/core@^1.20.0", "npm:@js-temporal/polyfill@~0.5.1", "npm:@nestjs/common@^11.0.1", "npm:@opentelemetry/api@^1.9.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb7a514c0..490a5aa8c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ catalogs: specifier: ^4.20250529.0 version: 4.20250529.0 '@fxts/core': - specifier: ^1.15.0 + specifier: ^1.20.0 version: 1.20.0 '@js-temporal/polyfill': specifier: ^0.5.1 @@ -275,7 +275,7 @@ importers: version: 1.6.3(patch_hash=56bd737eca4c1ba581d00bedd4fe307f6e48f782af59933eac54bb7d43206b99)(@algolia/client-search@5.29.0)(@types/node@22.19.1)(@types/react@18.3.23)(lightningcss@1.30.1)(postcss@8.5.6)(search-insights@2.17.3)(typescript@5.9.3) vitepress-plugin-group-icons: specifier: ^1.3.5 - version: 1.6.1(markdown-it@14.1.0)(vite@5.4.19(@types/node@22.19.1)(lightningcss@1.30.1)) + version: 1.6.1(markdown-it@14.1.0)(vite@7.1.3(@types/node@22.19.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.1)) vitepress-plugin-llms: specifier: ^1.1.0 version: 1.6.0 @@ -16425,6 +16425,22 @@ snapshots: fsevents: 2.3.3 lightningcss: 1.30.1 + vite@7.1.3(@types/node@22.19.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.1): + dependencies: + esbuild: 0.25.5 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.44.1 + tinyglobby: 0.2.14 + optionalDependencies: + '@types/node': 22.19.1 + fsevents: 2.3.3 + jiti: 2.5.1 + lightningcss: 1.30.1 + tsx: 4.20.3 + yaml: 2.8.1 + vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.1): dependencies: esbuild: 0.25.5 @@ -16445,13 +16461,13 @@ snapshots: optionalDependencies: vite: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.1) - vitepress-plugin-group-icons@1.6.1(markdown-it@14.1.0)(vite@5.4.19(@types/node@22.19.1)(lightningcss@1.30.1)): + vitepress-plugin-group-icons@1.6.1(markdown-it@14.1.0)(vite@7.1.3(@types/node@22.19.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.1)): dependencies: '@iconify-json/logos': 1.2.4 '@iconify-json/vscode-icons': 1.2.23 '@iconify/utils': 2.3.0 markdown-it: 14.1.0 - vite: 5.4.19(@types/node@22.19.1)(lightningcss@1.30.1) + vite: 7.1.3(@types/node@22.19.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.1) transitivePeerDependencies: - supports-color diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 91b1bf0f4..05c56f271 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -33,7 +33,7 @@ packages: catalog: "@cloudflare/workers-types": ^4.20250529.0 - "@fxts/core": ^1.15.0 + "@fxts/core": ^1.20.0 "@js-temporal/polyfill": ^0.5.1 "@logtape/file": ^1.2.2 "@logtape/logtape": ^1.2.2 From 62543cc253a06a2afa0b359b53a8e784459393c0 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Wed, 19 Nov 2025 03:54:01 +0000 Subject: [PATCH 08/41] Added @fxts/core in workspace --- packages/cli/deno.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/cli/deno.json b/packages/cli/deno.json index 9fa848cb8..9aa1666c3 100644 --- a/packages/cli/deno.json +++ b/packages/cli/deno.json @@ -4,7 +4,6 @@ "license": "MIT", "exports": "./src/mod.ts", "imports": { - "@fxts/core": "npm:@fxts/core@^1.15.0", "@hongminhee/localtunnel": "jsr:@hongminhee/localtunnel@^0.3.0", "@inquirer/prompts": "npm:@inquirer/prompts@^7.8.4", "@jimp/core": "npm:@jimp/core@^1.6.0", From 908a940768585d5d472ee56624eae64fcbac36c4 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Wed, 19 Nov 2025 08:13:51 +0000 Subject: [PATCH 09/41] Add utilities for linting --- packages/lint/src/lib/const.ts | 12 +++++++ packages/lint/src/lib/pred.ts | 31 ++++++++++++++++++ packages/lint/src/lib/test.ts | 54 ++++++++++++++++++++++++++++++++ packages/lint/src/lib/tracker.ts | 51 ++++++++++++++++++++++++++++++ 4 files changed, 148 insertions(+) create mode 100644 packages/lint/src/lib/const.ts create mode 100644 packages/lint/src/lib/pred.ts create mode 100644 packages/lint/src/lib/test.ts create mode 100644 packages/lint/src/lib/tracker.ts diff --git a/packages/lint/src/lib/const.ts b/packages/lint/src/lib/const.ts new file mode 100644 index 000000000..d001ba8a6 --- /dev/null +++ b/packages/lint/src/lib/const.ts @@ -0,0 +1,12 @@ +export const FEDERATION_SETUP = ` +import { + createFederation, + MemoryKvStore, + InProcessMessageQueue, +} from "@fedify/fedify"; + +const federation = createFederation({ + kv: new MemoryKvStore(), + queue: new InProcessMessageQueue(), +}); +` as const; diff --git a/packages/lint/src/lib/pred.ts b/packages/lint/src/lib/pred.ts new file mode 100644 index 000000000..a3bca3d90 --- /dev/null +++ b/packages/lint/src/lib/pred.ts @@ -0,0 +1,31 @@ +import { isNil, isObject } from "@fxts/core"; + +export function isFederationObject(node: unknown): boolean { + if (!isObject(node) || isNil(node)) return false; + const n = node as Record; + + // createFederation() 또는 createFederationBuilder() 함수 호출 결과인 경우 + if (n.type === "CallExpression") { + const callee = n.callee as Record; + if (callee.type === "Identifier") { + const name = callee.name as string; + return /^create(Federation|FederationBuilder)$/i.test(name); + } + return false; + } + + // Identifier인 경우: federation이라는 이름의 변수 + if (n.type === "Identifier") { + const name = n.name as string; + // 정확히 'federation'이거나 'Federation'으로 끝나는 변수명만 허용 + return name === "federation" || name.endsWith("Federation"); + } + + // ObjectExpression (객체 리터럴)은 명백히 Federation이 아님 + if (n.type === "ObjectExpression") { + return false; + } + + // 그 외의 경우는 검사 대상으로 간주 (보수적 접근) + return true; +} diff --git a/packages/lint/src/lib/test.ts b/packages/lint/src/lib/test.ts new file mode 100644 index 000000000..d7be09b6a --- /dev/null +++ b/packages/lint/src/lib/test.ts @@ -0,0 +1,54 @@ +import { isNil } from "@fxts/core"; +import { assert, assertEquals, assertGreater } from "jsr:@std/assert"; +import { FEDERATION_SETUP } from "./const.ts"; + +const LINT_PLUGIN_NAME = "fedify-lint-test"; + +export function testDenoLint( + { + code, + rule, + ruleName, + federationSetup = FEDERATION_SETUP, + expectedError, + }: { + code: string; + rule: Deno.lint.Rule; + ruleName: string; + federationSetup?: string | false; + expectedError?: string; + }, +) { + const plugin: Deno.lint.Plugin = { + name: LINT_PLUGIN_NAME, + rules: { [ruleName]: rule }, + }; + + const diagnostics = Deno.lint.runPlugin( + plugin, + ruleName + ".test.ts", + `${federationSetup ? federationSetup : ""}\n\n${code}`, + ); + + if (isNil(expectedError)) { + assertEquals( + diagnostics.length, + 0, + "Should not report issues when id property is present", + ); + } else { + assertGreater( + diagnostics.length, + 0, + "Expected at least one diagnostic error but found none", + ); + const lintId = `${LINT_PLUGIN_NAME}/${ruleName}`; + const matched = diagnostics.some((diag) => + diag.message.includes(expectedError) + ); + assert( + matched, + `Expected ${lintId} to report but it did not.`, + ); + } +} diff --git a/packages/lint/src/lib/tracker.ts b/packages/lint/src/lib/tracker.ts new file mode 100644 index 000000000..9b4a83c23 --- /dev/null +++ b/packages/lint/src/lib/tracker.ts @@ -0,0 +1,51 @@ +/** + * Helper to track variable names that store the result of createFederation() or createFederationBuilder() calls + */ +export function trackFederationVariables() { + const federationVariables = new Set(); + + return { + VariableDeclarator(node: unknown) { + if (typeof node !== "object" || node === null) return; + const n = node as Record; + const init = n.init as Record | null | undefined; + const id = n.id as Record; + + if ( + init?.type === "CallExpression" && + typeof init.callee === "object" && init.callee !== null + ) { + const callee = init.callee as Record; + if (callee.type === "Identifier" && typeof callee.name === "string") { + if (/^create(Federation|FederationBuilder)$/i.test(callee.name)) { + if (id.type === "Identifier" && typeof id.name === "string") { + federationVariables.add(id.name); + } + } + } + } + }, + + isFederationVariable(name: string): boolean { + return federationVariables.has(name); + }, + + isFederationObject(obj: unknown): boolean { + if (typeof obj !== "object" || obj === null) return false; + const o = obj as Record; + + if (o.type === "Identifier" && typeof o.name === "string") { + return federationVariables.has(o.name); + } else if ( + o.type === "CallExpression" && + typeof o.callee === "object" && o.callee !== null + ) { + const callee = o.callee as Record; + if (callee.type === "Identifier" && typeof callee.name === "string") { + return /^create(Federation|FederationBuilder)$/i.test(callee.name); + } + } + return false; + }, + }; +} From f1261e961a6fc09ff314a2c022e92f005c4a5552 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Wed, 19 Nov 2025 08:14:35 +0000 Subject: [PATCH 10/41] Implement `actor-id-required` rule and test of it --- packages/lint/src/rules/actor-id-required.ts | 102 ++++++++++++- .../lint/src/tests/actor-id-required.test.ts | 135 ++++++++++++++++++ 2 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 packages/lint/src/tests/actor-id-required.test.ts diff --git a/packages/lint/src/rules/actor-id-required.ts b/packages/lint/src/rules/actor-id-required.ts index e3fa99bc3..94a330400 100644 --- a/packages/lint/src/rules/actor-id-required.ts +++ b/packages/lint/src/rules/actor-id-required.ts @@ -1,2 +1,102 @@ -const actorIdRequired: Deno.lint.Rule = {}; +import { filter, isNil, isObject, pipe, some } from "@fxts/core"; +import { trackFederationVariables } from "../lib/tracker.ts"; + +export const ACTOR_ID_REQUIRED = "actor-id-required" as const; + +const actorIdRequired: Deno.lint.Rule = { + create(context) { + const tracker = trackFederationVariables(); + + return { + VariableDeclarator: tracker.VariableDeclarator, + + CallExpression(node) { + if ( + node.callee.type === "MemberExpression" && + node.callee.property.type === "Identifier" && + node.callee.property.name === "setActorDispatcher" && + node.arguments.length >= 2 + ) { + if (!tracker.isFederationObject(node.callee.object)) return; + + const dispatcherArg = node.arguments[1]; + + if ( + dispatcherArg.type === "ArrowFunctionExpression" || + dispatcherArg.type === "FunctionExpression" + ) { + const hasIdProperty = checkForIdProperty(dispatcherArg.body); + + if (!hasIdProperty) { + context.report({ + node: dispatcherArg, + message: + "Actor dispatcher must return an actor with an `id` property. Use `Context.getActorUri(identifier)` to set it.", + }); + } + } + } + }, + }; + }, +}; + export default actorIdRequired; + +function hasIdProperty(prop: unknown): boolean { + if (!isObject(prop) || isNil(prop)) return false; + const p = prop as Record; + const key = p.key as Record; + return p.type === "Property" && + key.type === "Identifier" && + key.name === "id"; +} + +function checkObjectExpression(obj: Record): boolean { + if (!Array.isArray(obj.properties)) return false; + return pipe( + obj.properties, + filter(isObject), + filter((p): p is Record => !isNil(p)), + some(hasIdProperty), + ); +} + +function checkReturnStatement(n: Record): boolean { + if (!n.argument) return false; + const arg = n.argument as Record; + + if ( + arg.type === "NewExpression" && Array.isArray(arg.arguments) && + arg.arguments.length > 0 + ) { + const objArg = arg.arguments[0] as Record; + if (objArg.type === "ObjectExpression") { + return checkObjectExpression(objArg); + } + } + + if (arg.type === "ObjectExpression") { + return checkObjectExpression(arg); + } + + return false; +} + +function checkForIdProperty(node: unknown): boolean { + if (!isObject(node) || isNil(node)) return false; + const n = node as Record; + + if (n.type === "ReturnStatement") { + return checkReturnStatement(n); + } + + if (n.type === "BlockStatement" && Array.isArray(n.body)) { + return pipe( + n.body, + some(checkForIdProperty), + ); + } + + return false; +} diff --git a/packages/lint/src/tests/actor-id-required.test.ts b/packages/lint/src/tests/actor-id-required.test.ts new file mode 100644 index 000000000..53e64c025 --- /dev/null +++ b/packages/lint/src/tests/actor-id-required.test.ts @@ -0,0 +1,135 @@ +import { test } from "node:test"; +import { testDenoLint } from "../lib/test.ts"; +import { + ACTOR_ID_REQUIRED as ruleName, + default as rule, +} from "../rules/actor-id-required.ts"; + +test(`${ruleName}: ✅ Good - \`setActorDispatcher\` called on non-Federation object`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return new Person({ + name: "John Doe", + }); + }); + `, + rule, + ruleName, + federationSetup: ` + const federation = { + setActorDispatcher: () => {} + }; + `, + }); +}); + +test(`${ruleName}: ✅ Good - with \`id\` property`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + name: "John Doe", + }); + }); + `, + rule, + ruleName, + }); +}); + +test(`${ruleName}: ✅ Good - object literal with \`id\``, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return { + id: ctx.getActorUri(identifier), + name: "John Doe", + }; + }); + `, + rule, + ruleName, + }); +}); + +test(`${ruleName}: ✅ Good - BlockStatement with \`id\``, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + const name = "John Doe"; + return new Person({ + id: ctx.getActorUri(identifier), + name, + }); + }); + `, + rule, + ruleName, + }); +}); + +test(`${ruleName}: ❌ Bad - without \`id\` property`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return new Person({ + name: "John Doe", + }); + }); + `, + rule, + ruleName, + expectedError: + "Actor dispatcher must return an actor with an `id` property. Use `Context.getActorUri(identifier)` to set it.", + }); +}); + +test(`${ruleName}: ❌ Bad - returning empty object`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return new Person({}); + }); + `, + rule, + ruleName, + expectedError: + "Actor dispatcher must return an actor with an `id` property", + }); +}); + +test(`${ruleName}: ❌ Bad - object literal without \`id\``, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return { + name: "John Doe", + followers: ctx.getFollowersUri(identifier), + }; + }); + `, + rule, + ruleName, + expectedError: + "Actor dispatcher must return an actor with an `id` property", + }); +}); + +test(`${ruleName}: ❌ Bad - BlockStatement without \`id\``, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + const actor = new Person({ + name: "John Doe", + }); + return actor; + }); + `, + rule, + ruleName, + expectedError: + "Actor dispatcher must return an actor with an `id` property", + }); +}); From 2ab5b8446b69fe10455de7cdce37469bba21a7d1 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Wed, 19 Nov 2025 08:14:46 +0000 Subject: [PATCH 11/41] Implement `actor-id-mismatch` rule and test of it --- packages/lint/src/rules/actor-id-mismatch.ts | 171 +++++++++++++++++- .../lint/src/tests/actor-id-mismatch.test.ts | 89 +++++++++ 2 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 packages/lint/src/tests/actor-id-mismatch.test.ts diff --git a/packages/lint/src/rules/actor-id-mismatch.ts b/packages/lint/src/rules/actor-id-mismatch.ts index d60d64ab1..3cc91fbb7 100644 --- a/packages/lint/src/rules/actor-id-mismatch.ts +++ b/packages/lint/src/rules/actor-id-mismatch.ts @@ -1,2 +1,171 @@ -const actorIdMismatch: Deno.lint.Rule = {}; +import { filter, isNil, isObject, pipe, some } from "@fxts/core"; +import { trackFederationVariables } from "../lib/tracker.ts"; + +export const ACTOR_ID_MISMATCH = "actor-id-mismatch" as const; + +const actorIdMismatch: Deno.lint.Rule = { + create(context) { + const tracker = trackFederationVariables(); + + return { + VariableDeclarator: tracker.VariableDeclarator, + + CallExpression(node) { + if ( + node.callee.type === "MemberExpression" && + node.callee.property.type === "Identifier" && + node.callee.property.name === "setActorDispatcher" && + node.arguments.length >= 2 + ) { + if (!tracker.isFederationObject(node.callee.object)) return; + + const dispatcherArg = node.arguments[1]; + + // Analyze dispatcher function + if ( + dispatcherArg.type === "ArrowFunctionExpression" || + dispatcherArg.type === "FunctionExpression" + ) { + // Extract function parameter names (context, identifier) + const params = dispatcherArg.params; + if (params.length < 2) return; + + const contextParam = params[0].type === "Identifier" + ? params[0].name + : null; + const identifierParam = params[1].type === "Identifier" + ? params[1].name + : null; + + if (!contextParam || !identifierParam) return; + + // Check if id property matches getActorUri call + const hasCorrectId = checkIdMatchesGetActorUri( + dispatcherArg.body, + contextParam, + identifierParam, + ); + + if (!hasCorrectId) { + context.report({ + node: dispatcherArg, + message: + `Actor's \`id\` property must match \`${contextParam}.getActorUri(${identifierParam})\`. Ensure you're using the correct context method.`, + }); + } + } + } + }, + }; + }, +}; + export default actorIdMismatch; + +// Declare isGetActorUriCall first (used in hasCorrectIdProperty) +function isGetActorUriCall( + node: unknown, + ctxName: string, + idName: string, +): boolean { + if (!isObject(node) || isNil(node)) return false; + const n = node as Record; + if (n.type !== "CallExpression") return false; + + const callee = n.callee as Record; + if (callee.type !== "MemberExpression") return false; + + const object = callee.object as Record; + const property = callee.property as Record; + const args = n.arguments as unknown[]; + + return object.type === "Identifier" && + object.name === ctxName && + property.type === "Identifier" && + property.name === "getActorUri" && + Array.isArray(args) && + args.length === 1 && + pipe( + args[0] as Record, + (arg) => arg.type === "Identifier" && arg.name === idName, + ); +} + +function hasCorrectIdProperty( + ctxName: string, + idName: string, + prop: unknown, +): boolean { + if (!isObject(prop) || isNil(prop)) return false; + const p = prop as Record; + const key = p.key as Record; + return p.type === "Property" && + key.type === "Identifier" && + key.name === "id" && + isGetActorUriCall(p.value, ctxName, idName); +} + +function checkObjectExpression( + obj: Record, + ctxName: string, + idName: string, +): boolean { + if (!Array.isArray(obj.properties)) return false; + return pipe( + obj.properties, + filter(isObject), + filter((p): p is Record => !isNil(p)), + some((prop) => hasCorrectIdProperty(ctxName, idName, prop)), + ); +} + +function checkReturnStatement( + n: Record, + ctxName: string, + idName: string, +): boolean { + if (!n.argument) return false; + const arg = n.argument as Record; + + // Pattern: new Person({ id: ctx.getActorUri(identifier) }) + if ( + arg.type === "NewExpression" && Array.isArray(arg.arguments) && + arg.arguments.length > 0 + ) { + const objArg = arg.arguments[0] as Record; + if (objArg.type === "ObjectExpression") { + return checkObjectExpression(objArg, ctxName, idName); + } + } + + // Pattern: { id: ctx.getActorUri(identifier) } + if (arg.type === "ObjectExpression") { + return checkObjectExpression(arg, ctxName, idName); + } + + return false; +} + +function checkIdMatchesGetActorUri( + node: unknown, + ctxName: string, + idName: string, +): boolean { + if (!isObject(node) || isNil(node)) return false; + const n = node as Record; + + // Find ReturnStatement + if (n.type === "ReturnStatement") { + return checkReturnStatement(n, ctxName, idName); + } + + // Recursively check if BlockStatement + if (n.type === "BlockStatement" && Array.isArray(n.body)) { + return pipe( + n.body, + some((stmt) => checkIdMatchesGetActorUri(stmt, ctxName, idName)), + ); + } + + return false; +} diff --git a/packages/lint/src/tests/actor-id-mismatch.test.ts b/packages/lint/src/tests/actor-id-mismatch.test.ts new file mode 100644 index 000000000..446f932b5 --- /dev/null +++ b/packages/lint/src/tests/actor-id-mismatch.test.ts @@ -0,0 +1,89 @@ +import { test } from "node:test"; +import { testDenoLint } from "../lib/test.ts"; +import { + ACTOR_ID_MISMATCH as ruleName, + default as rule, +} from "../rules/actor-id-mismatch.ts"; + +test(`${ruleName}: ✅ Good - \`setActorDispatcher\` called on non-Federation object`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return new Person({ + name: "John Doe", + }); + }); + `, + rule, + ruleName, + federationSetup: ` + const federation = { + setActorDispatcher: () => {} + }; + `, + }); +}); + +test(`${ruleName}: ✅ Good - \`id\` from \`ctx.getActorUri(identifier)\``, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + name: "John Doe", + }); + }); + `, + rule, + ruleName, + }); +}); + +test(`${ruleName}: ❌ Bad - \`id\` from not using \`ctx.getActorUri(identifier)\``, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return new Person({ + id: new URL("https://example.com/user/john"), + name: "John Doe", + }); + }); + `, + rule, + ruleName, + expectedError: + "Actor's `id` property must match `ctx.getActorUri(identifier)`", + }); + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + const someOtherVariable = new URL("https://example.com/user/john"); + return new Person({ + id: someOtherVariable, + name: "John Doe", + }); + }); + `, + rule, + ruleName, + expectedError: + "Actor's `id` property must match `ctx.getActorUri(identifier)`", + }); +}); + +test(`${ruleName}: ❌ Bad - \`id\` using wrong context method`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return new Person({ + id: ctx.getInboxUri(identifier), + name: "John Doe", + }); + }); + `, + rule, + ruleName, + expectedError: + "Actor's `id` property must match `ctx.getActorUri(identifier)`", + }); +}); From 2a5aeb058d51fe2bd76135c2e32ef9de72731ab0 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Wed, 19 Nov 2025 08:22:12 +0000 Subject: [PATCH 12/41] Add constants for linting rule names and update imports --- packages/lint/src/mod.ts | 144 ++++++++++++------ .../rules/actor-assertion-method-required.ts | 3 + .../rules/actor-featured-property-mismatch.ts | 3 + .../rules/actor-featured-property-required.ts | 3 + .../actor-featured-tags-property-mismatch.ts | 3 + .../actor-featured-tags-property-required.ts | 3 + .../actor-followers-property-mismatch.ts | 3 + .../actor-followers-property-required.ts | 3 + .../actor-following-property-mismatch.ts | 3 + .../actor-following-property-required.ts | 3 + .../rules/actor-inbox-property-mismatch.ts | 2 + .../rules/actor-inbox-property-required.ts | 2 + .../rules/actor-liked-property-mismatch.ts | 2 + .../rules/actor-liked-property-required.ts | 2 + .../rules/actor-outbox-property-mismatch.ts | 2 + .../rules/actor-outbox-property-required.ts | 2 + .../src/rules/actor-public-key-required.ts | 2 + .../actor-shared-inbox-property-mismatch.ts | 3 + .../actor-shared-inbox-property-required.ts | 3 + .../collection-filtering-not-implemented.ts | 3 + .../rules/ed25519-key-required-for-proof.ts | 2 + .../rsa-key-required-for-http-signature.ts | 3 + .../rsa-key-required-for-ld-signature.ts | 3 + 23 files changed, 154 insertions(+), 48 deletions(-) diff --git a/packages/lint/src/mod.ts b/packages/lint/src/mod.ts index 88b4ef561..0075ed649 100644 --- a/packages/lint/src/mod.ts +++ b/packages/lint/src/mod.ts @@ -1,55 +1,103 @@ -import actorAssertionMethodRequired from "./rules/actor-assertion-method-required.ts"; -import actorFeaturedPropertyMismatch from "./rules/actor-featured-property-mismatch.ts"; -import actorFeaturedPropertyRequired from "./rules/actor-featured-property-required.ts"; -import actorFeaturedTagsPropertyMismatch from "./rules/actor-featured-tags-property-mismatch.ts"; -import actorFeaturedTagsPropertyRequired from "./rules/actor-featured-tags-property-required.ts"; -import actorFollowersPropertyMismatch from "./rules/actor-followers-property-mismatch.ts"; -import actorFollowersPropertyRequired from "./rules/actor-followers-property-required.ts"; -import actorFollowingPropertyMismatch from "./rules/actor-following-property-mismatch.ts"; -import actorFollowingPropertyRequired from "./rules/actor-following-property-required.ts"; -import actorIdMismatch from "./rules/actor-id-mismatch.ts"; -import actorIdRequired from "./rules/actor-id-required.ts"; -import actorInboxPropertyMismatch from "./rules/actor-inbox-property-mismatch.ts"; -import actorInboxPropertyRequired from "./rules/actor-inbox-property-required.ts"; -import actorLikedPropertyMismatch from "./rules/actor-liked-property-mismatch.ts"; -import actorLikedPropertyRequired from "./rules/actor-liked-property-required.ts"; -import actorOutboxPropertyMismatch from "./rules/actor-outbox-property-mismatch.ts"; -import actorOutboxPropertyRequired from "./rules/actor-outbox-property-required.ts"; -import actorPublicKeyRequired from "./rules/actor-public-key-required.ts"; -import actorSharedInboxPropertyMismatch from "./rules/actor-shared-inbox-property-mismatch.ts"; -import actorSharedInboxPropertyRequired from "./rules/actor-shared-inbox-property-required.ts"; -import collectionFilteringNotImplemented from "./rules/collection-filtering-not-implemented.ts"; -import ed25519KeyRequiredForProof from "./rules/ed25519-key-required-for-proof.ts"; -import rsaKeyRequiredForHttpSignature from "./rules/rsa-key-required-for-http-signature.ts"; -import rsaKeyRequiredForLdSignature from "./rules/rsa-key-required-for-ld-signature.ts"; +import actorAssertionMethodRequired, { + ACTOR_ASSERTION_METHOD_REQUIRED, +} from "./rules/actor-assertion-method-required.ts"; +import actorFeaturedPropertyMismatch, { + ACTOR_FEATURED_PROPERTY_MISMATCH, +} from "./rules/actor-featured-property-mismatch.ts"; +import actorFeaturedPropertyRequired, { + ACTOR_FEATURED_PROPERTY_REQUIRED, +} from "./rules/actor-featured-property-required.ts"; +import actorFeaturedTagsPropertyMismatch, { + ACTOR_FEATURED_TAGS_PROPERTY_MISMATCH, +} from "./rules/actor-featured-tags-property-mismatch.ts"; +import actorFeaturedTagsPropertyRequired, { + ACTOR_FEATURED_TAGS_PROPERTY_REQUIRED, +} from "./rules/actor-featured-tags-property-required.ts"; +import actorFollowersPropertyMismatch, { + ACTOR_FOLLOWERS_PROPERTY_MISMATCH, +} from "./rules/actor-followers-property-mismatch.ts"; +import actorFollowersPropertyRequired, { + ACTOR_FOLLOWERS_PROPERTY_REQUIRED, +} from "./rules/actor-followers-property-required.ts"; +import actorFollowingPropertyMismatch, { + ACTOR_FOLLOWING_PROPERTY_MISMATCH, +} from "./rules/actor-following-property-mismatch.ts"; +import actorFollowingPropertyRequired, { + ACTOR_FOLLOWING_PROPERTY_REQUIRED, +} from "./rules/actor-following-property-required.ts"; +import actorIdMismatch, { + ACTOR_ID_MISMATCH, +} from "./rules/actor-id-mismatch.ts"; +import actorIdRequired, { + ACTOR_ID_REQUIRED, +} from "./rules/actor-id-required.ts"; +import actorInboxPropertyMismatch, { + ACTOR_INBOX_PROPERTY_MISMATCH, +} from "./rules/actor-inbox-property-mismatch.ts"; +import actorInboxPropertyRequired, { + ACTOR_INBOX_PROPERTY_REQUIRED, +} from "./rules/actor-inbox-property-required.ts"; +import actorLikedPropertyMismatch, { + ACTOR_LIKED_PROPERTY_MISMATCH, +} from "./rules/actor-liked-property-mismatch.ts"; +import actorLikedPropertyRequired, { + ACTOR_LIKED_PROPERTY_REQUIRED, +} from "./rules/actor-liked-property-required.ts"; +import actorOutboxPropertyMismatch, { + ACTOR_OUTBOX_PROPERTY_MISMATCH, +} from "./rules/actor-outbox-property-mismatch.ts"; +import actorOutboxPropertyRequired, { + ACTOR_OUTBOX_PROPERTY_REQUIRED, +} from "./rules/actor-outbox-property-required.ts"; +import actorPublicKeyRequired, { + ACTOR_PUBLIC_KEY_REQUIRED, +} from "./rules/actor-public-key-required.ts"; +import actorSharedInboxPropertyMismatch, { + ACTOR_SHARED_INBOX_PROPERTY_MISMATCH, +} from "./rules/actor-shared-inbox-property-mismatch.ts"; +import actorSharedInboxPropertyRequired, { + ACTOR_SHARED_INBOX_PROPERTY_REQUIRED, +} from "./rules/actor-shared-inbox-property-required.ts"; +import collectionFilteringNotImplemented, { + COLLECTION_FILTERING_NOT_IMPLEMENTED, +} from "./rules/collection-filtering-not-implemented.ts"; +import ed25519KeyRequiredForProof, { + ED25519_KEY_REQUIRED_FOR_PROOF, +} from "./rules/ed25519-key-required-for-proof.ts"; +import rsaKeyRequiredForHttpSignature, { + RSA_KEY_REQUIRED_FOR_HTTP_SIGNATURE, +} from "./rules/rsa-key-required-for-http-signature.ts"; +import rsaKeyRequiredForLdSignature, { + RSA_KEY_REQUIRED_FOR_LD_SIGNATURE, +} from "./rules/rsa-key-required-for-ld-signature.ts"; const plugin: Deno.lint.Plugin = { name: "@fedify/lint", rules: { - "actor-id-required": actorIdRequired, - "actor-id-mismatch": actorIdMismatch, - "actor-following-property-required": actorFollowingPropertyRequired, - "actor-following-property-mismatch": actorFollowingPropertyMismatch, - "actor-followers-property-required": actorFollowersPropertyRequired, - "actor-followers-property-mismatch": actorFollowersPropertyMismatch, - "actor-outbox-property-required": actorOutboxPropertyRequired, - "actor-outbox-property-mismatch": actorOutboxPropertyMismatch, - "actor-liked-property-required": actorLikedPropertyRequired, - "actor-liked-property-mismatch": actorLikedPropertyMismatch, - "actor-featured-property-required": actorFeaturedPropertyRequired, - "actor-featured-property-mismatch": actorFeaturedPropertyMismatch, - "actor-featured-tags-property-required": actorFeaturedTagsPropertyRequired, - "actor-featured-tags-property-mismatch": actorFeaturedTagsPropertyMismatch, - "collection-filtering-not-implemented": collectionFilteringNotImplemented, - "actor-inbox-property-required": actorInboxPropertyRequired, - "actor-inbox-property-mismatch": actorInboxPropertyMismatch, - "actor-shared-inbox-property-required": actorSharedInboxPropertyRequired, - "actor-shared-inbox-property-mismatch": actorSharedInboxPropertyMismatch, - "actor-public-key-required": actorPublicKeyRequired, - "actor-assertion-method-required": actorAssertionMethodRequired, - "rsa-key-required-for-http-signature": rsaKeyRequiredForHttpSignature, - "rsa-key-required-for-ld-signature": rsaKeyRequiredForLdSignature, - "ed25519-key-required-for-proof": ed25519KeyRequiredForProof, + [ACTOR_ID_MISMATCH]: actorIdMismatch, + [ACTOR_ID_REQUIRED]: actorIdRequired, + [ACTOR_FOLLOWING_PROPERTY_REQUIRED]: actorFollowingPropertyRequired, + [ACTOR_FOLLOWING_PROPERTY_MISMATCH]: actorFollowingPropertyMismatch, + [ACTOR_FOLLOWERS_PROPERTY_REQUIRED]: actorFollowersPropertyRequired, + [ACTOR_FOLLOWERS_PROPERTY_MISMATCH]: actorFollowersPropertyMismatch, + [ACTOR_OUTBOX_PROPERTY_REQUIRED]: actorOutboxPropertyRequired, + [ACTOR_OUTBOX_PROPERTY_MISMATCH]: actorOutboxPropertyMismatch, + [ACTOR_LIKED_PROPERTY_REQUIRED]: actorLikedPropertyRequired, + [ACTOR_LIKED_PROPERTY_MISMATCH]: actorLikedPropertyMismatch, + [ACTOR_FEATURED_PROPERTY_REQUIRED]: actorFeaturedPropertyRequired, + [ACTOR_FEATURED_PROPERTY_MISMATCH]: actorFeaturedPropertyMismatch, + [ACTOR_FEATURED_TAGS_PROPERTY_REQUIRED]: actorFeaturedTagsPropertyRequired, + [ACTOR_FEATURED_TAGS_PROPERTY_MISMATCH]: actorFeaturedTagsPropertyMismatch, + [COLLECTION_FILTERING_NOT_IMPLEMENTED]: collectionFilteringNotImplemented, + [ACTOR_INBOX_PROPERTY_REQUIRED]: actorInboxPropertyRequired, + [ACTOR_INBOX_PROPERTY_MISMATCH]: actorInboxPropertyMismatch, + [ACTOR_SHARED_INBOX_PROPERTY_REQUIRED]: actorSharedInboxPropertyRequired, + [ACTOR_SHARED_INBOX_PROPERTY_MISMATCH]: actorSharedInboxPropertyMismatch, + [ACTOR_PUBLIC_KEY_REQUIRED]: actorPublicKeyRequired, + [ACTOR_ASSERTION_METHOD_REQUIRED]: actorAssertionMethodRequired, + [RSA_KEY_REQUIRED_FOR_HTTP_SIGNATURE]: rsaKeyRequiredForHttpSignature, + [RSA_KEY_REQUIRED_FOR_LD_SIGNATURE]: rsaKeyRequiredForLdSignature, + [ED25519_KEY_REQUIRED_FOR_PROOF]: ed25519KeyRequiredForProof, }, }; diff --git a/packages/lint/src/rules/actor-assertion-method-required.ts b/packages/lint/src/rules/actor-assertion-method-required.ts index dde5540a5..dbd6cd420 100644 --- a/packages/lint/src/rules/actor-assertion-method-required.ts +++ b/packages/lint/src/rules/actor-assertion-method-required.ts @@ -1,2 +1,5 @@ +export const ACTOR_ASSERTION_METHOD_REQUIRED = + "actor-assertion-method-required"; + const actorAssertionMethodRequired: Deno.lint.Rule = {}; export default actorAssertionMethodRequired; diff --git a/packages/lint/src/rules/actor-featured-property-mismatch.ts b/packages/lint/src/rules/actor-featured-property-mismatch.ts index d644a1966..22d7d22b2 100644 --- a/packages/lint/src/rules/actor-featured-property-mismatch.ts +++ b/packages/lint/src/rules/actor-featured-property-mismatch.ts @@ -1,2 +1,5 @@ +export const ACTOR_FEATURED_PROPERTY_MISMATCH = + "actor-featured-property-mismatch"; + const actorFeaturedPropertyMismatch: Deno.lint.Rule = {}; export default actorFeaturedPropertyMismatch; diff --git a/packages/lint/src/rules/actor-featured-property-required.ts b/packages/lint/src/rules/actor-featured-property-required.ts index 7bed738a4..e73685de1 100644 --- a/packages/lint/src/rules/actor-featured-property-required.ts +++ b/packages/lint/src/rules/actor-featured-property-required.ts @@ -1,2 +1,5 @@ +export const ACTOR_FEATURED_PROPERTY_REQUIRED = + "actor-featured-property-required"; + const actorFeaturedPropertyRequired: Deno.lint.Rule = {}; export default actorFeaturedPropertyRequired; diff --git a/packages/lint/src/rules/actor-featured-tags-property-mismatch.ts b/packages/lint/src/rules/actor-featured-tags-property-mismatch.ts index 78e61889c..fa867f830 100644 --- a/packages/lint/src/rules/actor-featured-tags-property-mismatch.ts +++ b/packages/lint/src/rules/actor-featured-tags-property-mismatch.ts @@ -1,2 +1,5 @@ +export const ACTOR_FEATURED_TAGS_PROPERTY_MISMATCH = + "actor-featured-tags-property-mismatch"; + const actorFeaturedTagsPropertyMismatch: Deno.lint.Rule = {}; export default actorFeaturedTagsPropertyMismatch; diff --git a/packages/lint/src/rules/actor-featured-tags-property-required.ts b/packages/lint/src/rules/actor-featured-tags-property-required.ts index 8d8e7a83e..4972149a1 100644 --- a/packages/lint/src/rules/actor-featured-tags-property-required.ts +++ b/packages/lint/src/rules/actor-featured-tags-property-required.ts @@ -1,2 +1,5 @@ +export const ACTOR_FEATURED_TAGS_PROPERTY_REQUIRED = + "actor-featured-tags-property-required"; + const actorFeaturedTagsPropertyRequired: Deno.lint.Rule = {}; export default actorFeaturedTagsPropertyRequired; diff --git a/packages/lint/src/rules/actor-followers-property-mismatch.ts b/packages/lint/src/rules/actor-followers-property-mismatch.ts index e1142d3a2..c71684dbf 100644 --- a/packages/lint/src/rules/actor-followers-property-mismatch.ts +++ b/packages/lint/src/rules/actor-followers-property-mismatch.ts @@ -1,2 +1,5 @@ +export const ACTOR_FOLLOWERS_PROPERTY_MISMATCH = + "actor-followers-property-mismatch"; + const actorFollowersPropertyMismatch: Deno.lint.Rule = {}; export default actorFollowersPropertyMismatch; diff --git a/packages/lint/src/rules/actor-followers-property-required.ts b/packages/lint/src/rules/actor-followers-property-required.ts index 0cc54f820..55da56005 100644 --- a/packages/lint/src/rules/actor-followers-property-required.ts +++ b/packages/lint/src/rules/actor-followers-property-required.ts @@ -1,2 +1,5 @@ +export const ACTOR_FOLLOWERS_PROPERTY_REQUIRED = + "actor-followers-property-required"; + const actorFollowersPropertyRequired: Deno.lint.Rule = {}; export default actorFollowersPropertyRequired; diff --git a/packages/lint/src/rules/actor-following-property-mismatch.ts b/packages/lint/src/rules/actor-following-property-mismatch.ts index c78dddcc6..1c88a5585 100644 --- a/packages/lint/src/rules/actor-following-property-mismatch.ts +++ b/packages/lint/src/rules/actor-following-property-mismatch.ts @@ -1,2 +1,5 @@ +export const ACTOR_FOLLOWING_PROPERTY_MISMATCH = + "actor-following-property-mismatch"; + const actorFollowingPropertyMismatch: Deno.lint.Rule = {}; export default actorFollowingPropertyMismatch; diff --git a/packages/lint/src/rules/actor-following-property-required.ts b/packages/lint/src/rules/actor-following-property-required.ts index 978a6a4cf..7916b8f5e 100644 --- a/packages/lint/src/rules/actor-following-property-required.ts +++ b/packages/lint/src/rules/actor-following-property-required.ts @@ -1,2 +1,5 @@ +export const ACTOR_FOLLOWING_PROPERTY_REQUIRED = + "actor-following-property-required"; + const actorFollowingPropertyRequired: Deno.lint.Rule = {}; export default actorFollowingPropertyRequired; diff --git a/packages/lint/src/rules/actor-inbox-property-mismatch.ts b/packages/lint/src/rules/actor-inbox-property-mismatch.ts index 3c95b44d0..2ceec6e92 100644 --- a/packages/lint/src/rules/actor-inbox-property-mismatch.ts +++ b/packages/lint/src/rules/actor-inbox-property-mismatch.ts @@ -1,2 +1,4 @@ +export const ACTOR_INBOX_PROPERTY_MISMATCH = "actor-inbox-property-mismatch"; + const actorInboxPropertyMismatch: Deno.lint.Rule = {}; export default actorInboxPropertyMismatch; diff --git a/packages/lint/src/rules/actor-inbox-property-required.ts b/packages/lint/src/rules/actor-inbox-property-required.ts index 0135a9308..5ac7de701 100644 --- a/packages/lint/src/rules/actor-inbox-property-required.ts +++ b/packages/lint/src/rules/actor-inbox-property-required.ts @@ -1,2 +1,4 @@ +export const ACTOR_INBOX_PROPERTY_REQUIRED = "actor-inbox-property-required"; + const actorInboxPropertyRequired: Deno.lint.Rule = {}; export default actorInboxPropertyRequired; diff --git a/packages/lint/src/rules/actor-liked-property-mismatch.ts b/packages/lint/src/rules/actor-liked-property-mismatch.ts index 3e13af942..09c2db89e 100644 --- a/packages/lint/src/rules/actor-liked-property-mismatch.ts +++ b/packages/lint/src/rules/actor-liked-property-mismatch.ts @@ -1,2 +1,4 @@ +export const ACTOR_LIKED_PROPERTY_MISMATCH = "actor-liked-property-mismatch"; + const actorLikedPropertyMismatch: Deno.lint.Rule = {}; export default actorLikedPropertyMismatch; diff --git a/packages/lint/src/rules/actor-liked-property-required.ts b/packages/lint/src/rules/actor-liked-property-required.ts index c402618c5..f17e3ad1f 100644 --- a/packages/lint/src/rules/actor-liked-property-required.ts +++ b/packages/lint/src/rules/actor-liked-property-required.ts @@ -1,2 +1,4 @@ +export const ACTOR_LIKED_PROPERTY_REQUIRED = "actor-liked-property-required"; + const actorLikedPropertyRequired: Deno.lint.Rule = {}; export default actorLikedPropertyRequired; diff --git a/packages/lint/src/rules/actor-outbox-property-mismatch.ts b/packages/lint/src/rules/actor-outbox-property-mismatch.ts index 749e64109..3ac7feb1b 100644 --- a/packages/lint/src/rules/actor-outbox-property-mismatch.ts +++ b/packages/lint/src/rules/actor-outbox-property-mismatch.ts @@ -1,2 +1,4 @@ +export const ACTOR_OUTBOX_PROPERTY_MISMATCH = "actor-outbox-property-mismatch"; + const actorOutboxPropertyMismatch: Deno.lint.Rule = {}; export default actorOutboxPropertyMismatch; diff --git a/packages/lint/src/rules/actor-outbox-property-required.ts b/packages/lint/src/rules/actor-outbox-property-required.ts index 81012f7fc..4fefc7b92 100644 --- a/packages/lint/src/rules/actor-outbox-property-required.ts +++ b/packages/lint/src/rules/actor-outbox-property-required.ts @@ -1,2 +1,4 @@ +export const ACTOR_OUTBOX_PROPERTY_REQUIRED = "actor-outbox-property-required"; + const actorOutboxPropertyRequired: Deno.lint.Rule = {}; export default actorOutboxPropertyRequired; diff --git a/packages/lint/src/rules/actor-public-key-required.ts b/packages/lint/src/rules/actor-public-key-required.ts index c41b7a105..75f4640db 100644 --- a/packages/lint/src/rules/actor-public-key-required.ts +++ b/packages/lint/src/rules/actor-public-key-required.ts @@ -1,2 +1,4 @@ +export const ACTOR_PUBLIC_KEY_REQUIRED = "actor-public-key-required"; + const actorPublicKeyRequired: Deno.lint.Rule = {}; export default actorPublicKeyRequired; diff --git a/packages/lint/src/rules/actor-shared-inbox-property-mismatch.ts b/packages/lint/src/rules/actor-shared-inbox-property-mismatch.ts index a858916bf..de89b4e61 100644 --- a/packages/lint/src/rules/actor-shared-inbox-property-mismatch.ts +++ b/packages/lint/src/rules/actor-shared-inbox-property-mismatch.ts @@ -1,2 +1,5 @@ +export const ACTOR_SHARED_INBOX_PROPERTY_MISMATCH = + "actor-shared-inbox-property-mismatch"; + const actorSharedInboxPropertyMismatch: Deno.lint.Rule = {}; export default actorSharedInboxPropertyMismatch; diff --git a/packages/lint/src/rules/actor-shared-inbox-property-required.ts b/packages/lint/src/rules/actor-shared-inbox-property-required.ts index 1b6b58a58..6c5b1e196 100644 --- a/packages/lint/src/rules/actor-shared-inbox-property-required.ts +++ b/packages/lint/src/rules/actor-shared-inbox-property-required.ts @@ -1,2 +1,5 @@ +export const ACTOR_SHARED_INBOX_PROPERTY_REQUIRED = + "actor-shared-inbox-property-required"; + const actorSharedInboxPropertyRequired: Deno.lint.Rule = {}; export default actorSharedInboxPropertyRequired; diff --git a/packages/lint/src/rules/collection-filtering-not-implemented.ts b/packages/lint/src/rules/collection-filtering-not-implemented.ts index 36d230c25..48f6f2bce 100644 --- a/packages/lint/src/rules/collection-filtering-not-implemented.ts +++ b/packages/lint/src/rules/collection-filtering-not-implemented.ts @@ -1,2 +1,5 @@ +export const COLLECTION_FILTERING_NOT_IMPLEMENTED = + "collection-filtering-not-implemented"; + const collectionFilteringNotImplemented: Deno.lint.Rule = {}; export default collectionFilteringNotImplemented; diff --git a/packages/lint/src/rules/ed25519-key-required-for-proof.ts b/packages/lint/src/rules/ed25519-key-required-for-proof.ts index 40a233832..032c61995 100644 --- a/packages/lint/src/rules/ed25519-key-required-for-proof.ts +++ b/packages/lint/src/rules/ed25519-key-required-for-proof.ts @@ -1,2 +1,4 @@ +export const ED25519_KEY_REQUIRED_FOR_PROOF = "ed25519-key-required-for-proof"; + const ed25519KeyRequiredForProof: Deno.lint.Rule = {}; export default ed25519KeyRequiredForProof; diff --git a/packages/lint/src/rules/rsa-key-required-for-http-signature.ts b/packages/lint/src/rules/rsa-key-required-for-http-signature.ts index bafec2302..b0b574cf3 100644 --- a/packages/lint/src/rules/rsa-key-required-for-http-signature.ts +++ b/packages/lint/src/rules/rsa-key-required-for-http-signature.ts @@ -1,2 +1,5 @@ +export const RSA_KEY_REQUIRED_FOR_HTTP_SIGNATURE = + "rsa-key-required-for-http-signature"; + const rsaKeyRequiredForHttpSignature: Deno.lint.Rule = {}; export default rsaKeyRequiredForHttpSignature; diff --git a/packages/lint/src/rules/rsa-key-required-for-ld-signature.ts b/packages/lint/src/rules/rsa-key-required-for-ld-signature.ts index 771826031..5e36e0813 100644 --- a/packages/lint/src/rules/rsa-key-required-for-ld-signature.ts +++ b/packages/lint/src/rules/rsa-key-required-for-ld-signature.ts @@ -1,2 +1,5 @@ +export const RSA_KEY_REQUIRED_FOR_LD_SIGNATURE = + "rsa-key-required-for-ld-signature"; + const rsaKeyRequiredForLdSignature: Deno.lint.Rule = {}; export default rsaKeyRequiredForLdSignature; From c05c473abd777e96c7b48c681d9ad8b222e25168 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Thu, 27 Nov 2025 05:49:09 +0000 Subject: [PATCH 13/41] Renamed key-related rules --- packages/lint/src/mod.ts | 18 ++++++------------ .../rules/ed25519-key-required-for-proof.ts | 4 ---- .../lint/src/rules/ed25519-key-required.ts | 4 ++++ .../rsa-key-required-for-http-signature.ts | 5 ----- .../rules/rsa-key-required-for-ld-signature.ts | 5 ----- packages/lint/src/rules/rsa-key-required.ts | 4 ++++ 6 files changed, 14 insertions(+), 26 deletions(-) delete mode 100644 packages/lint/src/rules/ed25519-key-required-for-proof.ts create mode 100644 packages/lint/src/rules/ed25519-key-required.ts delete mode 100644 packages/lint/src/rules/rsa-key-required-for-http-signature.ts delete mode 100644 packages/lint/src/rules/rsa-key-required-for-ld-signature.ts create mode 100644 packages/lint/src/rules/rsa-key-required.ts diff --git a/packages/lint/src/mod.ts b/packages/lint/src/mod.ts index 0075ed649..d4927588f 100644 --- a/packages/lint/src/mod.ts +++ b/packages/lint/src/mod.ts @@ -61,15 +61,10 @@ import actorSharedInboxPropertyRequired, { import collectionFilteringNotImplemented, { COLLECTION_FILTERING_NOT_IMPLEMENTED, } from "./rules/collection-filtering-not-implemented.ts"; -import ed25519KeyRequiredForProof, { - ED25519_KEY_REQUIRED_FOR_PROOF, -} from "./rules/ed25519-key-required-for-proof.ts"; -import rsaKeyRequiredForHttpSignature, { - RSA_KEY_REQUIRED_FOR_HTTP_SIGNATURE, -} from "./rules/rsa-key-required-for-http-signature.ts"; -import rsaKeyRequiredForLdSignature, { - RSA_KEY_REQUIRED_FOR_LD_SIGNATURE, -} from "./rules/rsa-key-required-for-ld-signature.ts"; +import ed25519KeyRequired, { + ED25519_KEY_REQUIRED, +} from "./rules/ed25519-key-required.ts"; +import rsaKeyRequired, { RSA_KEY_REQUIRED } from "./rules/rsa-key-required.ts"; const plugin: Deno.lint.Plugin = { name: "@fedify/lint", @@ -95,9 +90,8 @@ const plugin: Deno.lint.Plugin = { [ACTOR_SHARED_INBOX_PROPERTY_MISMATCH]: actorSharedInboxPropertyMismatch, [ACTOR_PUBLIC_KEY_REQUIRED]: actorPublicKeyRequired, [ACTOR_ASSERTION_METHOD_REQUIRED]: actorAssertionMethodRequired, - [RSA_KEY_REQUIRED_FOR_HTTP_SIGNATURE]: rsaKeyRequiredForHttpSignature, - [RSA_KEY_REQUIRED_FOR_LD_SIGNATURE]: rsaKeyRequiredForLdSignature, - [ED25519_KEY_REQUIRED_FOR_PROOF]: ed25519KeyRequiredForProof, + [RSA_KEY_REQUIRED]: rsaKeyRequired, + [ED25519_KEY_REQUIRED]: ed25519KeyRequired, }, }; diff --git a/packages/lint/src/rules/ed25519-key-required-for-proof.ts b/packages/lint/src/rules/ed25519-key-required-for-proof.ts deleted file mode 100644 index 032c61995..000000000 --- a/packages/lint/src/rules/ed25519-key-required-for-proof.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const ED25519_KEY_REQUIRED_FOR_PROOF = "ed25519-key-required-for-proof"; - -const ed25519KeyRequiredForProof: Deno.lint.Rule = {}; -export default ed25519KeyRequiredForProof; diff --git a/packages/lint/src/rules/ed25519-key-required.ts b/packages/lint/src/rules/ed25519-key-required.ts new file mode 100644 index 000000000..e2c60fe02 --- /dev/null +++ b/packages/lint/src/rules/ed25519-key-required.ts @@ -0,0 +1,4 @@ +export const ED25519_KEY_REQUIRED = "ed25519-key-required"; + +const ed25519KeyRequired: Deno.lint.Rule = {}; +export default ed25519KeyRequired; diff --git a/packages/lint/src/rules/rsa-key-required-for-http-signature.ts b/packages/lint/src/rules/rsa-key-required-for-http-signature.ts deleted file mode 100644 index b0b574cf3..000000000 --- a/packages/lint/src/rules/rsa-key-required-for-http-signature.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const RSA_KEY_REQUIRED_FOR_HTTP_SIGNATURE = - "rsa-key-required-for-http-signature"; - -const rsaKeyRequiredForHttpSignature: Deno.lint.Rule = {}; -export default rsaKeyRequiredForHttpSignature; diff --git a/packages/lint/src/rules/rsa-key-required-for-ld-signature.ts b/packages/lint/src/rules/rsa-key-required-for-ld-signature.ts deleted file mode 100644 index 5e36e0813..000000000 --- a/packages/lint/src/rules/rsa-key-required-for-ld-signature.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const RSA_KEY_REQUIRED_FOR_LD_SIGNATURE = - "rsa-key-required-for-ld-signature"; - -const rsaKeyRequiredForLdSignature: Deno.lint.Rule = {}; -export default rsaKeyRequiredForLdSignature; diff --git a/packages/lint/src/rules/rsa-key-required.ts b/packages/lint/src/rules/rsa-key-required.ts new file mode 100644 index 000000000..99a6f6d33 --- /dev/null +++ b/packages/lint/src/rules/rsa-key-required.ts @@ -0,0 +1,4 @@ +export const RSA_KEY_REQUIRED = "rsa-key-required"; + +const rsaKeyRequired: Deno.lint.Rule = {}; +export default rsaKeyRequired; From 2fa04fb89aeeb3abe3140e78e00cb5b99bc5c346 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Thu, 4 Dec 2025 10:35:22 +0000 Subject: [PATCH 14/41] deno linter --- deno.json | 2 +- packages/lint/deno.json | 4 +- packages/lint/src/lib/const.ts | 53 + packages/lint/src/lib/messages.ts | 44 + .../lint/src/lib/mismatch-rule-factory.ts | 347 ++++ packages/lint/src/lib/pred.ts | 176 +- packages/lint/src/lib/property-checker.ts | 242 +++ .../lint/src/lib/required-rule-factory.ts | 124 ++ packages/lint/src/lib/test-templates.ts | 1485 +++++++++++++++++ packages/lint/src/lib/tracker.ts | 67 +- packages/lint/src/lib/types.ts | 49 + packages/lint/src/lib/utils.ts | 20 + packages/lint/src/mod.ts | 12 +- .../rules/actor-assertion-method-required.ts | 10 +- .../rules/actor-featured-property-mismatch.ts | 11 +- .../rules/actor-featured-property-required.ts | 11 +- .../actor-featured-tags-property-mismatch.ts | 11 +- .../actor-featured-tags-property-required.ts | 11 +- .../actor-followers-property-mismatch.ts | 11 +- .../actor-followers-property-required.ts | 11 +- .../actor-following-property-mismatch.ts | 11 +- .../actor-following-property-required.ts | 11 +- packages/lint/src/rules/actor-id-mismatch.ts | 179 +- packages/lint/src/rules/actor-id-required.ts | 112 +- .../rules/actor-inbox-property-mismatch.ts | 12 +- .../rules/actor-inbox-property-required.ts | 11 +- .../rules/actor-liked-property-mismatch.ts | 12 +- .../rules/actor-liked-property-required.ts | 11 +- .../rules/actor-outbox-property-mismatch.ts | 12 +- .../rules/actor-outbox-property-required.ts | 11 +- .../src/rules/actor-public-key-required.ts | 10 +- .../actor-shared-inbox-property-mismatch.ts | 12 +- .../actor-shared-inbox-property-required.ts | 11 +- .../collection-filtering-not-implemented.ts | 113 +- .../lint/src/rules/ed25519-key-required.ts | 4 - packages/lint/src/rules/rsa-key-required.ts | 4 - .../actor-assertion-method-required.test.ts | 233 +++ .../actor-featured-property-mismatch.test.ts | 59 + .../actor-featured-property-required.test.ts | 89 + ...or-featured-tags-property-mismatch.test.ts | 59 + ...or-featured-tags-property-required.test.ts | 89 + .../actor-followers-property-mismatch.test.ts | 59 + .../actor-followers-property-required.test.ts | 89 + .../actor-following-property-mismatch.test.ts | 59 + .../actor-following-property-required.test.ts | 89 + .../lint/src/tests/actor-id-mismatch.test.ts | 160 +- .../lint/src/tests/actor-id-required.test.ts | 59 +- .../actor-inbox-property-mismatch.test.ts | 59 + .../actor-inbox-property-required.test.ts | 89 + .../actor-liked-property-mismatch.test.ts | 59 + .../actor-liked-property-required.test.ts | 89 + .../actor-outbox-property-mismatch.test.ts | 59 + .../actor-outbox-property-required.test.ts | 89 + .../tests/actor-public-key-required.test.ts | 211 +++ ...tor-shared-inbox-property-mismatch.test.ts | 59 + ...tor-shared-inbox-property-required.test.ts | 89 + ...llection-filtering-not-implemented.test.ts | 127 ++ packages/lint/src/tests/edge-cases.test.ts | 217 +++ 58 files changed, 5027 insertions(+), 401 deletions(-) create mode 100644 packages/lint/src/lib/messages.ts create mode 100644 packages/lint/src/lib/mismatch-rule-factory.ts create mode 100644 packages/lint/src/lib/property-checker.ts create mode 100644 packages/lint/src/lib/required-rule-factory.ts create mode 100644 packages/lint/src/lib/test-templates.ts create mode 100644 packages/lint/src/lib/types.ts create mode 100644 packages/lint/src/lib/utils.ts delete mode 100644 packages/lint/src/rules/ed25519-key-required.ts delete mode 100644 packages/lint/src/rules/rsa-key-required.ts create mode 100644 packages/lint/src/tests/actor-assertion-method-required.test.ts create mode 100644 packages/lint/src/tests/actor-featured-property-mismatch.test.ts create mode 100644 packages/lint/src/tests/actor-featured-property-required.test.ts create mode 100644 packages/lint/src/tests/actor-featured-tags-property-mismatch.test.ts create mode 100644 packages/lint/src/tests/actor-featured-tags-property-required.test.ts create mode 100644 packages/lint/src/tests/actor-followers-property-mismatch.test.ts create mode 100644 packages/lint/src/tests/actor-followers-property-required.test.ts create mode 100644 packages/lint/src/tests/actor-following-property-mismatch.test.ts create mode 100644 packages/lint/src/tests/actor-following-property-required.test.ts create mode 100644 packages/lint/src/tests/actor-inbox-property-mismatch.test.ts create mode 100644 packages/lint/src/tests/actor-inbox-property-required.test.ts create mode 100644 packages/lint/src/tests/actor-liked-property-mismatch.test.ts create mode 100644 packages/lint/src/tests/actor-liked-property-required.test.ts create mode 100644 packages/lint/src/tests/actor-outbox-property-mismatch.test.ts create mode 100644 packages/lint/src/tests/actor-outbox-property-required.test.ts create mode 100644 packages/lint/src/tests/actor-public-key-required.test.ts create mode 100644 packages/lint/src/tests/actor-shared-inbox-property-mismatch.test.ts create mode 100644 packages/lint/src/tests/actor-shared-inbox-property-required.test.ts create mode 100644 packages/lint/src/tests/collection-filtering-not-implemented.test.ts create mode 100644 packages/lint/src/tests/edge-cases.test.ts diff --git a/deno.json b/deno.json index e89c2cb4a..ccf76af21 100644 --- a/deno.json +++ b/deno.json @@ -24,7 +24,7 @@ "imports": { "@cloudflare/workers-types": "npm:@cloudflare/workers-types@^4.20250529.0", "@david/dax": "jsr:@david/dax@^0.43.2", - "@fxts/core": "npm:@fxts/core@^1.20.0", + "@fxts/core": "npm:@fxts/core@^1.21.1", "@js-temporal/polyfill": "npm:@js-temporal/polyfill@^0.5.1", "@logtape/file": "jsr:@logtape/file@^1.2.2", "@logtape/logtape": "jsr:@logtape/logtape@^1.2.2", diff --git a/packages/lint/deno.json b/packages/lint/deno.json index d6507d570..f7ed9d990 100644 --- a/packages/lint/deno.json +++ b/packages/lint/deno.json @@ -5,5 +5,7 @@ "exports": { ".": "./src/mod.ts" }, - "imports": {} + "tasks": { + "test": "deno test --allow-all" + } } diff --git a/packages/lint/src/lib/const.ts b/packages/lint/src/lib/const.ts index d001ba8a6..1b75b9fdf 100644 --- a/packages/lint/src/lib/const.ts +++ b/packages/lint/src/lib/const.ts @@ -10,3 +10,56 @@ const federation = createFederation({ queue: new InProcessMessageQueue(), }); ` as const; + +/** + * Mapping of actor property names to their corresponding Context method names + * and dispatcher methods. + * Used by lint rules to validate property existence and correct method usage. + */ +export const properties = { + id: { + name: "id", + getter: "getActorUri", + setter: "setActorDispatcher", + }, + following: { + name: "following", + getter: "getFollowingUri", + setter: "setFollowingDispatcher", + }, + followers: { + name: "followers", + getter: "getFollowersUri", + setter: "setFollowersDispatcher", + }, + outbox: { + name: "outbox", + getter: "getOutboxUri", + setter: "setOutboxDispatcher", + }, + inbox: { + name: "inbox", + getter: "getInboxUri", + setter: "setInboxListeners", + }, + liked: { + name: "liked", + getter: "getLikedUri", + setter: "setLikedDispatcher", + }, + featured: { + name: "featured", + getter: "getFeaturedUri", + setter: "setFeaturedDispatcher", + }, + featuredTags: { + name: "featuredTags", + getter: "getFeaturedTagsUri", + setter: "setFeaturedTagsDispatcher", + }, + sharedInbox: { + name: "endpoints.sharedInbox", + getter: "getInboxUri", + setter: "setInboxListeners", + }, +} as const; diff --git a/packages/lint/src/lib/messages.ts b/packages/lint/src/lib/messages.ts new file mode 100644 index 000000000..144e8ee5a --- /dev/null +++ b/packages/lint/src/lib/messages.ts @@ -0,0 +1,44 @@ +import { getArticle } from "./utils.ts"; + +/** + * Generates error message for *-required rules. + * Used when a property is missing from the actor dispatcher return value. + * + * @param propertyName - The property name (e.g., "id", "inbox", "following") + */ +export const actorPropertyRequired = (propertyName: string): string => + `Actor dispatcher must return an actor with ${ + getArticle(propertyName) + } \`${propertyName}\` property.`; + +/** + * Generates error message for publicKey and assertionMethod required rules. + * These use getActorKeyPairs instead of a property-specific getter. + * + * @param propertyName - The property name (e.g., "publicKey", "assertionMethod") + */ +export const actorKeyPropertyRequired = (propertyName: string): string => + `${ + actorPropertyRequired(propertyName) + } Use \`Context.getActorKeyPairs(identifier)\` to retrieve key pairs.`; + +/** + * Generates error message for *-mismatch rules. + * Used when a property exists but uses the wrong context method. + * + * @param propertyName - The property name or path (e.g., "id", "endpoints.sharedInbox") + * @param expectedCall - The expected method call (e.g., "ctx.getActorUri(identifier)") + */ +export const actorPropertyMismatch = ( + propertyName: string, + expectedCall: string, +): string => + `Actor's \`${propertyName}\` property must match \`${expectedCall}\`. Ensure you're using the correct context method.`; + +/** + * Error message for collection filtering not implemented. + */ +export const COLLECTION_FILTERING_NOT_IMPLEMENTED_ERROR = + "Collection dispatcher should implement filtering to avoid large response " + + "payloads. Add a fourth parameter (filter) to handle filtering. " + + "See: https://fedify.dev/manual/collections#filtering-by-server"; diff --git a/packages/lint/src/lib/mismatch-rule-factory.ts b/packages/lint/src/lib/mismatch-rule-factory.ts new file mode 100644 index 000000000..3687894d5 --- /dev/null +++ b/packages/lint/src/lib/mismatch-rule-factory.ts @@ -0,0 +1,347 @@ +import { + filter, + isArray, + isEmpty, + isNil, + isObject, + negate, + pipe, + pipeLazy, + prop, + some, + toArray, +} from "@fxts/core"; +import { actorPropertyMismatch } from "./messages.ts"; +import { + allOf, + hasIdentifierKey, + hasMemberExpressionCallee, + isFunction, + isNodeName, + isNodeType, + isSetActorDispatcherCall, +} from "./pred.ts"; +import { trackFederationVariables } from "./tracker.ts"; +import type { + ASTNode, + FunctionNode, + MethodCallContext, + MismatchRuleConfig, +} from "./types.ts"; +import { eq } from "./utils.ts"; + +/** + * Checks if a value is a valid AST node object. + */ +const isASTNode = (node: unknown): node is ASTNode => + isObject(node) && !isNil(node); + +/** + * Filters and casts array items to ASTNode. + */ +const filterASTNodes = (items: unknown[]): ASTNode[] => + pipe( + items, + filter(isObject), + filter((p): p is ASTNode => !isNil(p)), + toArray, + ); + +const isIdentifierWithName = + (name: T) => + (node: N): node is N & { + "type": "Identifier"; + "name": T; + } => allOf(isNodeType("Identifier"), isNodeName(name))(node); + +/** + * Checks if a node is a CallExpression calling the expected context method. + */ +const isExpectedMethodCall = ( + node: + | Deno.lint.Expression + | Deno.lint.AssignmentPattern + | Deno.lint.TSEmptyBodyFunctionExpression, + { ctxName, idName, methodName, requiresIdentifier }: MethodCallContext, +): boolean => { + if ( + !isNodeType("CallExpression")(node) || + !hasMemberExpressionCallee(node) || + !isIdentifierWithName(ctxName)(node.callee.object) || + !isIdentifierWithName(methodName)(node.callee.property) + ) return false; + + if (!requiresIdentifier) return isEmpty(node.arguments); + return allOf( + negate(isEmpty), + some(isIdentifierWithName(idName)), + )(node.arguments); +}; + +const isPropertyWithKeyName = (path: string) => +( + node: ASTNode, +): node is { type: "Property"; key: { name: string } & ASTNode } & ASTNode => + allOf( + isNodeType("Property"), + hasIdentifierKey, + pipeLazy( + prop("key")<{ key: { name: string } }>, + prop("name"), + eq(path), + ) as (value: unknown) => boolean, + )(node); + +/** + * Creates a property existence checker for the given property path. + * Only checks if the property exists, not its value. + */ +const createPropertyExistenceChecker = (propertyPath: string) => { + const path = propertyPath.split("."); + const checkPropertyExists = (path: string[]) => (node: ASTNode): boolean => { + if (!isPropertyWithKeyName(path[0])(node)) return false; + + // Base case: last property in path + if (path.length === 1) return true; + + // Nested case: check the nested object + const value = node.value; + if (!isNodeType("ObjectExpression")(value)) return false; + + return pipe( + value.properties, + filterASTNodes, + some(checkPropertyExists(path.slice(1))), + ); + }; + + return (prop: ASTNode): boolean => + allOf(isASTNode, checkPropertyExists(path))(prop); +}; + +/** + * Creates a property value checker for the given property path. + * Handles both simple properties (e.g., "id") and nested properties (e.g., ["endpoints", "sharedInbox"]). + */ +const createPropertyValueChecker = ( + propertyPath: string, + ctx: MethodCallContext, +) => { + const path = propertyPath.split("."); + const checkPropertyValue = (path: string[]) => (prop: ASTNode): boolean => { + if (!isPropertyWithKeyName(path[0])(prop)) return false; + + // Base case: last property in path + const value = prop.value; + if (path.length === 1) { + return isExpectedMethodCall(value, ctx); + } + + // Nested case: check the nested object + if (!isNodeType("ObjectExpression")(value)) { + return false; + } + + return pipe( + value.properties, + filterASTNodes, + some(checkPropertyValue(path.slice(1))), + ); + }; + + return (prop: ASTNode): boolean => + allOf(isASTNode, checkPropertyValue(path))(prop); +}; + +/** + * Checks if an ObjectExpression contains a property with the correct method call. + */ +const checkObjectExpression = + (propertyChecker: (prop: ASTNode) => boolean) => + (obj: N): boolean => { + if (!isArray(obj.properties)) return false; + return pipe(obj.properties, filterASTNodes, some(propertyChecker)); + }; + +/** + * Checks if a ConditionalExpression (ternary operator) has the correct property value in both branches. + */ +const checkConditionalExpression = + (propertyChecker: (prop: ASTNode) => boolean) => + (node: Deno.lint.ConditionalExpression): boolean => { + const checkBranch = (branch: ASTNode): boolean => { + // Handle nested ternary operators + if (isNodeType("ConditionalExpression")(branch)) { + return checkConditionalExpression(propertyChecker)(branch); + } + + // Pattern: new Person({ property: ctx.method() }) + if (isNodeType("NewExpression")(branch)) { + return branch.arguments.filter(isNodeType("ObjectExpression")) + .some(checkObjectExpression(propertyChecker)); + } + + // Pattern: { property: ctx.method() } + if (isNodeType("ObjectExpression")(branch)) { + return checkObjectExpression(propertyChecker)(branch); + } + + return false; + }; + + return checkBranch(node.consequent) && checkBranch(node.alternate); + }; + +/** + * Checks if a ReturnStatement contains the correct property value. + */ +const checkReturnStatement = + (propertyChecker: (prop: ASTNode) => boolean) => (node: ASTNode): boolean => { + const arg = prop("argument")(node); + if (!isASTNode(arg)) return false; + + // Pattern: ternary operator + if (isNodeType("ConditionalExpression")(arg)) { + return checkConditionalExpression(propertyChecker)(arg); + } + + // Pattern: new Person({ property: ctx.method() }) + if (isNodeType("NewExpression")(arg)) { + return arg.arguments.filter(isNodeType("ObjectExpression")) + .some(checkObjectExpression(propertyChecker)); + } + + // Pattern: { property: ctx.method() } + if (isNodeType("ObjectExpression")(arg)) { + return checkObjectExpression(propertyChecker)(arg); + } + + return false; + }; + +/** + * Recursively checks if a function body contains the correct property value. + */ +const checkFunctionBody = + (propertyChecker: (prop: ASTNode) => boolean) => (node: ASTNode): boolean => { + if (!isASTNode(node)) return false; + + if (isNodeType("ReturnStatement")(node)) { + return checkReturnStatement(propertyChecker)(node); + } + + if (isNodeType("BlockStatement")(node) && Array.isArray(node.body)) { + return node.body.some(checkFunctionBody(propertyChecker)); + } + + // Pattern: arrow function direct return with object literal + // e.g., (ctx, identifier) => ({ id: ctx.getActorUri(identifier) }) + if (isNodeType("ObjectExpression")(node)) { + return checkObjectExpression(propertyChecker)(node); + } + + // Pattern: arrow function direct return with new expression + // e.g., (ctx, identifier) => new Person({ id: ctx.getActorUri(identifier) }) + if (isNodeType("NewExpression")(node)) { + return node.arguments.filter(isNodeType("ObjectExpression")) + .some(checkObjectExpression(propertyChecker)); + } + + // Pattern: arrow function direct return with ternary + if (isNodeType("ConditionalExpression")(node)) { + return checkConditionalExpression(propertyChecker)(node); + } + + return false; + }; + +/** + * Extracts parameter names from a function. + */ +const extractParams = ( + fn: FunctionNode, +): [string | null, string | null] => { + const params = fn.params; + if (params.length < 2) return [null, null]; + + return params.slice(0, 2).map(getNameIfIdentifier) as [ + string | null, + string | null, + ]; +}; + +const getNameIfIdentifier = (node: Deno.lint.Parameter): string | null => + isNodeType("Identifier")(node) ? prop("name")(node) : null; +/** + * Creates a lint rule that checks if a property uses the correct context method. + * + * @param config Configuration object containing property path, method name, and identifier requirement + * @returns A Deno lint rule + */ +export const createMismatchRule = ( + { + propertyPath, + methodName, + requiresIdentifier = true, + }: MismatchRuleConfig, +): Deno.lint.Rule => { + return { + create(context) { + const tracker = trackFederationVariables(); + + return { + VariableDeclarator: tracker.VariableDeclarator, + + CallExpression(node) { + if ( + !isSetActorDispatcherCall(node) || + !hasMemberExpressionCallee(node) || + !tracker.isFederationObject(node.callee.object) + ) return; + + const dispatcherArg = node.arguments[1]; + if (!isFunction(dispatcherArg)) return; + + const [ctxName, idName] = extractParams(dispatcherArg); + if (!ctxName || !idName) return; + + const methodCallContext: MethodCallContext = { + ctxName, + idName, + methodName, + requiresIdentifier, + }; + + // Check if the property exists first + const existenceChecker = createPropertyExistenceChecker(propertyPath); + const hasProperty = checkFunctionBody(existenceChecker)( + dispatcherArg.body, + ); + + // If property doesn't exist, don't report (that's for *-required rules) + if (!hasProperty) return; + + // Property exists, now check if the value is correct + const valueChecker = createPropertyValueChecker( + propertyPath, + methodCallContext, + ); + const hasCorrectValue = checkFunctionBody(valueChecker)( + dispatcherArg.body, + ); + + if (!hasCorrectValue) { + const expectedCall = requiresIdentifier + ? `${ctxName}.${methodName}(${idName})` + : `${ctxName}.${methodName}()`; + + context.report({ + node: dispatcherArg, + message: actorPropertyMismatch(propertyPath, expectedCall), + }); + } + }, + }; + }, + }; +}; diff --git a/packages/lint/src/lib/pred.ts b/packages/lint/src/lib/pred.ts index a3bca3d90..a1646d4b0 100644 --- a/packages/lint/src/lib/pred.ts +++ b/packages/lint/src/lib/pred.ts @@ -1,31 +1,147 @@ -import { isNil, isObject } from "@fxts/core"; - -export function isFederationObject(node: unknown): boolean { - if (!isObject(node) || isNil(node)) return false; - const n = node as Record; - - // createFederation() 또는 createFederationBuilder() 함수 호출 결과인 경우 - if (n.type === "CallExpression") { - const callee = n.callee as Record; - if (callee.type === "Identifier") { - const name = callee.name as string; - return /^create(Federation|FederationBuilder)$/i.test(name); - } - return false; - } - - // Identifier인 경우: federation이라는 이름의 변수 - if (n.type === "Identifier") { - const name = n.name as string; - // 정확히 'federation'이거나 'Federation'으로 끝나는 변수명만 허용 - return name === "federation" || name.endsWith("Federation"); - } - - // ObjectExpression (객체 리터럴)은 명백히 Federation이 아님 - if (n.type === "ObjectExpression") { - return false; - } - - // 그 외의 경우는 검사 대상으로 간주 (보수적 접근) - return true; +import { isNil, isObject, isString, negate, pipe, prop } from "@fxts/core"; +import type { + ASTNode, + CallMemberExpression, + CallMemberExpressionWithIdentified, + FunctionNode, +} from "./types.ts"; +import { eq } from "./utils.ts"; + +interface Predicate { + (value: unknown): value is T; +} + +/** + * Combines multiple predicates with AND logic. + */ +export function allOf( + ...refinements: (((value: T) => boolean) | Predicate)[] +): (v: T) => v is S; +export function allOf( + ...predicates: ((value: T) => boolean)[] +): (value: T) => boolean; +export function allOf( + ...predicates: Array<(value: T) => boolean> +): (value: T) => boolean { + return (value: T): boolean => + predicates.every((predicate) => predicate(value)); } + +/** + * Type guard to check if a value is a valid AST node. + */ +export const isASTNode = (value: unknown): value is ASTNode => + allOf(isObject, negate(isNil), hasTypeProperty)(value); + +const hasTypeProperty = (node: N): node is N & { "type": string } => + pipe(node as { "type": unknown }, prop("type"), isString) as boolean; + +/** + * Checks if a node is of a specific type. + */ +export const isNodeType = + (type: T) => + (node: N): node is { "type": T } & N => + pipe( + node, + prop("type"), + eq(type), + ) as boolean; + +/** + * Checks if a node is of a specific name. + */ +export const isNodeName = + (name: T) => (node: N): node is N & { "name": T } => + pipe( + node as { "name": string }, + prop("name"), + eq(name), + ) as boolean; + +/** + * Checks if a node has a key that is an AST node. + */ +export const hasASTNodeKey = (node: N): node is N & { "key": ASTNode } => + pipe(node as { "key": ASTNode }, prop("key"), isASTNode); + +/** + * Checks if a node's key is an Identifier. + */ +export const hasIdentifierKey = ( + node: N, +): node is N & { "key": ASTNode & { type: "Identifier" } } => + pipe( + node as { "key": ASTNode }, + prop("key"), + allOf(isASTNode, isNodeType("Identifier")), + ); + +/** + * Checks if a node's callee is a MemberExpression. + */ +export const hasMemberExpressionCallee = ( + node: Deno.lint.CallExpression, +): node is CallMemberExpression => node.callee.type === "MemberExpression"; + +/** + * Checks if a node's callee property is an Identifier. + */ +export const hasIdentifierProperty = ( + node: CallMemberExpression, +): node is CallMemberExpressionWithIdentified => + node.callee.property.type === "Identifier"; + +/** + * Checks if a node's callee property name matches the given method name. + */ +export const hasMethodName = + (methodName: T) => + (node: N): node is N & { + callee: { property: { name: T } }; + } => node.callee.property.name === methodName; + +/** + * Checks if a CallExpression has minimum required arguments. + */ +export const hasMinArguments = + (min: number) => (node: Deno.lint.CallExpression): boolean => + node.arguments.length >= min; + +/** + * Checks if an expression is an arrow function. + */ +export const isArrowFunction = ( + expr: Deno.lint.Expression | Deno.lint.SpreadElement, +): expr is Deno.lint.ArrowFunctionExpression => + isNodeType("ArrowFunctionExpression")(expr); + +/** + * Checks if an expression is a function expression. + */ +export const isFunctionExpression = ( + expr: Deno.lint.Expression | Deno.lint.SpreadElement, +): expr is Deno.lint.FunctionExpression => + isNodeType("FunctionExpression")(expr); + +/** + * Checks if an expression is a function (arrow or regular). + */ +export const isFunction = ( + expr: Deno.lint.Expression | Deno.lint.SpreadElement, +): expr is FunctionNode => isArrowFunction(expr) || isFunctionExpression(expr); + +/** + * Checks if a CallExpression is a setActorDispatcher call with proper structure. + */ +export const isSetActorDispatcherCall = ( + node: N, +): node is N & CallMemberExpressionWithIdentified & { + callee: { property: Deno.lint.Identifier & { name: "setActorDispatcher" } }; +} => + allOf( + hasMemberExpressionCallee, + hasIdentifierProperty as (node: unknown) => boolean, + hasMethodName("setActorDispatcher") as (node: unknown) => boolean, + hasMinArguments(2), + )(node); diff --git a/packages/lint/src/lib/property-checker.ts b/packages/lint/src/lib/property-checker.ts new file mode 100644 index 000000000..e19673a4a --- /dev/null +++ b/packages/lint/src/lib/property-checker.ts @@ -0,0 +1,242 @@ +import { filter, pipe, pipeLazy, prop, some } from "@fxts/core"; +import { + allOf, + hasASTNodeKey, + hasIdentifierKey, + isASTNode, + isNodeType, +} from "./pred.ts"; +import type { ASTNode } from "./types.ts"; +import { eq } from "./utils.ts"; + +/** + * Checks if a node's key name matches the given property name. + */ +const keyNameMatches = + (propertyName: T) => + (node: N): node is N & { "key": ASTNode & { "name": T } } => + pipe( + node as { "key": { "name": string } }, + prop("key"), + allOf( + isASTNode, + pipeLazy( + prop("name"), + eq(propertyName), + ) as (value: { name: string }) => boolean, + ), + ) as boolean; + +/** + * Checks if a node has a key with a specific name. + */ +const hasKeyName = + (propertyName: T) => + (node: N): node is N & { "key": ASTNode & { "name": T } } => + allOf( + hasASTNodeKey, + hasIdentifierKey, + keyNameMatches(propertyName), + )(node as { "key": ASTNode & { "name": T } }); + +/** + * Checks if a node is a Property with an Identifier key of a specific name. + */ +const isPropertyWithName = + (propertyName: T) => + (node: N): node is N & { + "type": "Property"; + "key": ASTNode & { "name": T }; + } => + allOf( + isNodeType("Property"), + hasKeyName(propertyName), + )(node); + +/** + * Creates a predicate function that checks if a property has a specific name. + * @param propertyName The name of the property to check for + * @returns A predicate function that checks if the property exists + */ +export const createPropertyChecker = + (propertyName: T) => + (node: unknown): node is ASTNode & { + "type": "Property"; + "key": ASTNode & { "name": T }; + } => allOf(isASTNode, isPropertyWithName(propertyName))(node as ASTNode); + +/** + * Checks if a node has an ObjectExpression value. + */ +const hasObjectExpressionValue = ( + node: Deno.lint.Property, +): node is Deno.lint.Property & { "value": Deno.lint.ObjectExpression } => + pipe( + node as { "value": ASTNode }, + prop("value"), + allOf(isASTNode, isNodeType("ObjectExpression")), + ); + +/** + * Type guard to check if a node is a Property. + */ +const isProperty = (node: ASTNode): node is Deno.lint.Property => + isNodeType("Property")(node); + +/** + * Internal recursive checker for nested property paths. + * This avoids circular dependency between hasNestedProperty and createNestedPropertyChecker. + */ +const checkNestedPropertyPath = + (path: string[]) => (node: unknown): boolean => { + if (!isASTNode(node) || !hasKeyName(path[0])(node)) return false; + + // Base case: single property + if (path.length === 1) { + return isNodeType("Property")(node); + } + + // Recursive case: check nested properties + if (!isProperty(node)) return false; + if (!hasObjectExpressionValue(node)) return false; + + const properties = node.value.properties; + + // Check if any property matches the remaining path + return pipe( + properties, + filter(isASTNode), + some(checkNestedPropertyPath(path.slice(1))), + ); + }; + +/** + * Creates a predicate function that checks if a nested property exists. + * @param path Array of property names forming the path (e.g., ["endpoints", "sharedInbox"]) + * @returns A predicate function that checks if the nested property exists + */ +export const createNestedPropertyChecker = + (path: string[]) => (node: unknown): boolean => + checkNestedPropertyPath(path)(node); + +/** + * Checks if an ObjectExpression node contains a property. + * @param propertyChecker The predicate function to check properties + * @returns A function that checks the ObjectExpression + */ +export const checkObjectExpression = + (propertyChecker: (prop: unknown) => boolean) => (obj: ASTNode): boolean => + pipe( + obj as { properties: unknown[] }, + prop("properties"), + (properties) => + Array.isArray(properties) + ? pipe(properties, filter(isASTNode), some(propertyChecker)) + : false, + ); + +/** + * Extracts the first argument if it's an ObjectExpression. + */ +const extractFirstArgument = (node: ASTNode): + | ASTNode & { + type: "ObjectExpression"; + } + | null => + pipe( + node as { arguments: unknown[] }, + prop("arguments"), + (args) => { + if (!Array.isArray(args) || args.length === 0) return null; + const firstArg = args[0]; + return isASTNode(firstArg) && isNodeType("ObjectExpression")(firstArg) + ? firstArg + : null; + }, + ); + +/** + * Extracts ObjectExpression from NewExpression or direct return. + */ +const extractObjectExpression = ( + arg: ASTNode, +): ASTNode & { type: "ObjectExpression" } | null => { + if (isNodeType("ObjectExpression")(arg)) return arg; + if (isNodeType("NewExpression")(arg)) return extractFirstArgument(arg); + return null; +}; + +/** + * Checks if a ConditionalExpression (ternary operator) has the property in both branches. + * @param propertyChecker The predicate function to check properties + * @returns A function that checks the ConditionalExpression + */ +const checkConditionalExpression = + (propertyChecker: (prop: unknown) => boolean) => + (node: Deno.lint.ConditionalExpression): boolean => { + const consequent = node.consequent; + const alternate = node.alternate; + + // Check if both branches have the property + const checkBranch = (branch: ASTNode): boolean => { + // Handle nested ternary operators + if (isNodeType("ConditionalExpression")(branch)) { + return checkConditionalExpression(propertyChecker)(branch); + } + const objExpr = extractObjectExpression(branch); + return objExpr ? checkObjectExpression(propertyChecker)(objExpr) : false; + }; + + return checkBranch(consequent) && checkBranch(alternate); + }; + +/** + * Checks if a ReturnStatement node contains a property. + * @param propertyChecker The predicate function to check properties + * @returns A function that checks the ReturnStatement + */ +export const checkReturnStatement = + (propertyChecker: (prop: unknown) => boolean) => + ( + node: ASTNode, + ): node is ASTNode & { type: "ReturnStatement"; arg: unknown } => { + const arg = pipe(node as { argument: unknown }, prop("argument")); + if (!isASTNode(arg)) return false; + + // Handle ConditionalExpression (ternary operator) + if (isNodeType("ConditionalExpression")(arg)) { + return checkConditionalExpression(propertyChecker)(arg); + } + + const objExpr = extractObjectExpression(arg); + return objExpr ? checkObjectExpression(propertyChecker)(objExpr) : false; + }; + +/** + * Creates a function that recursively checks for a property in an AST node. + * @param propertyChecker The predicate function to check properties + * @returns A recursive function that checks the AST node + */ +export const createPropertySearcher = + (propertyChecker: (prop: unknown) => boolean) => + (node: unknown): node is + | Deno.lint.ReturnStatement + | Deno.lint.BlockStatement + | Deno.lint.ObjectExpression => { + if (!isASTNode(node)) return false; + + if (isNodeType("ReturnStatement")(node)) { + return checkReturnStatement(propertyChecker)(node); + } + + if (isNodeType("BlockStatement")(node)) { + return node.body.some(createPropertySearcher(propertyChecker)); + } + + // Handle arrow function with direct ObjectExpression body: () => ({...}) + if (isNodeType("ObjectExpression")(node)) { + return checkObjectExpression(propertyChecker)(node); + } + + return false; + }; diff --git a/packages/lint/src/lib/required-rule-factory.ts b/packages/lint/src/lib/required-rule-factory.ts new file mode 100644 index 000000000..ee0fcb467 --- /dev/null +++ b/packages/lint/src/lib/required-rule-factory.ts @@ -0,0 +1,124 @@ +import { + allOf, + hasIdentifierProperty, + hasMemberExpressionCallee, + hasMethodName, + isFunction, + isSetActorDispatcherCall, +} from "./pred.ts"; +import { + createNestedPropertyChecker, + createPropertyChecker, + createPropertySearcher, +} from "./property-checker.ts"; +import { trackFederationVariables } from "./tracker.ts"; +import type { + CallMemberExpression, + CallMemberExpressionWithIdentified, + FunctionNode, +} from "./types.ts"; + +/** + * Checks if a CallExpression is a specific dispatcher method call. + */ +const isDispatcherMethodCall = (methodName: string) => +( + node: Deno.lint.CallExpression, +): node is CallMemberExpression => + allOf( + hasMemberExpressionCallee, + hasIdentifierProperty, + hasMethodName(methodName), + )(node as CallMemberExpressionWithIdentified); + +/** + * Tracks dispatcher method calls on federation objects. + */ +export const createDispatcherTracker = ( + dispatcherMethod: string, + federationTracker: ReturnType, +) => { + let dispatcherConfigured = false; + + return { + isDispatcherConfigured: () => dispatcherConfigured, + checkDispatcherCall: (node: Deno.lint.CallExpression) => { + if ( + isDispatcherMethodCall(dispatcherMethod)(node) && + federationTracker.isFederationObject(node.callee.object) + ) { + dispatcherConfigured = true; + } + }, + }; +}; + +/** + * Stores actor dispatcher info for later validation. + */ +interface ActorDispatcherInfo { + node: Deno.lint.CallExpression; + dispatcherArg: FunctionNode; +} + +/** + * Internal configuration for the unified rule factory. + */ +interface InternalRequiredConfig { + propertyName: string; + dispatcherMethod: string; + errorMessage: string; +} + +/** + * Creates a required rule with the given configuration. + */ +export function createRequiredRule( + config: InternalRequiredConfig, +): Deno.lint.Rule { + const propertyPath = config.propertyName.split("."); + const propertyChecker = propertyPath.length === 1 + ? createPropertyChecker(propertyPath[0]) + : createNestedPropertyChecker(propertyPath); + const propertySearcher = createPropertySearcher(propertyChecker); + + return { + create(context) { + const federationTracker = trackFederationVariables(); + const dispatcherTracker = createDispatcherTracker( + config.dispatcherMethod, + federationTracker, + ); + const actorDispatchers: ActorDispatcherInfo[] = []; + + return { + VariableDeclarator: federationTracker.VariableDeclarator, + + CallExpression(node) { + dispatcherTracker.checkDispatcherCall(node); + + if (!isSetActorDispatcherCall(node)) return; + if (!federationTracker.isFederationObject(node.callee.object)) return; + + const dispatcherArg = node.arguments[1]; + if (isFunction(dispatcherArg)) { + actorDispatchers.push({ node, dispatcherArg }); + } + }, + + "Program:exit"() { + if (!dispatcherTracker.isDispatcherConfigured()) return; + + for (const { dispatcherArg } of actorDispatchers) { + if (!propertySearcher(dispatcherArg.body)) { + context.report({ + node: dispatcherArg, + message: config.errorMessage, + }); + } + } + }, + }; + }, + }; +} diff --git a/packages/lint/src/lib/test-templates.ts b/packages/lint/src/lib/test-templates.ts new file mode 100644 index 000000000..e1bec9394 --- /dev/null +++ b/packages/lint/src/lib/test-templates.ts @@ -0,0 +1,1485 @@ +import { properties } from "./const.ts"; +import { + actorKeyPropertyRequired, + actorPropertyMismatch, + actorPropertyRequired, +} from "./messages.ts"; +import { testDenoLint } from "./test.ts"; + +// deno-lint-ignore no-explicit-any +type Rule = any; + +// ============================================================================= +// Types +// ============================================================================= + +type PropertyKey = keyof typeof properties; + +interface TestConfig { + rule: Rule; + ruleName: string; +} + +// ============================================================================= +// Common Code Snippets +// ============================================================================= + +const createDispatcherCode = (content: string) => ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + ${content} + }); +`; + +const createChainedDispatcherCode = ( + content: string, + dispatcherMethod: string, +) => ` + federation + .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + ${content} + }) + .${dispatcherMethod}(async (ctx, identifier) => []); +`; + +const createSeparateDispatcherCode = ( + content: string, + dispatcherMethod: string, + isBefore: boolean, +) => { + const dispatcher = + `federation.${dispatcherMethod}(async (ctx, identifier) => []);`; + const actor = createDispatcherCode(content); + return isBefore ? `${dispatcher}\n${actor}` : `${actor}\n${dispatcher}`; +}; + +const createTestCode = ( + type: "class" | "literal", + propertyKey: PropertyKey, + includeProperty: boolean, +) => { + const prop = properties[propertyKey]; + const isNested = prop.name.includes("."); + + let propertyCode = ""; + if (includeProperty) { + if (isNested) { + // endpoints.sharedInbox + propertyCode = `endpoints: { sharedInbox: ctx.${prop.getter}() },`; + } else { + propertyCode = `${prop.name}: ctx.${prop.getter}(identifier),`; + } + } + + if (type === "class") { + return `return new Person({ + id: ctx.getActorUri(identifier), + ${propertyCode} + name: "John Doe", + });`; + } + return `return { + id: ctx.getActorUri(identifier), + ${propertyCode} + name: "John Doe", + };`; +}; + +// ============================================================================= +// Required Rule Tests (Standard Properties) +// ============================================================================= + +/** + * Creates required rule tests for standard properties that use a dispatcher + * (following, followers, outbox, liked, featured, featuredTags) + */ +export function createRequiredDispatcherRuleTests( + propertyKey: PropertyKey, + config: TestConfig, +) { + const { rule, ruleName } = config; + const prop = properties[propertyKey]; + const expectedError = actorPropertyRequired(prop.name); + + return { + // ✅ Good - non-Federation object + "non-federation object": () => { + testDenoLint({ + code: createDispatcherCode(createTestCode("class", propertyKey, false)), + rule, + ruleName, + federationSetup: ` + const federation = { setActorDispatcher: () => {} }; + `, + }); + }, + + // ✅ Good - dispatcher NOT configured + "dispatcher not configured": () => { + testDenoLint({ + code: createDispatcherCode(createTestCode("class", propertyKey, false)), + rule, + ruleName, + }); + }, + + // ✅ Good - dispatcher configured BEFORE (chained) + "dispatcher before chained with property": () => { + testDenoLint({ + code: createChainedDispatcherCode( + createTestCode("class", propertyKey, true), + prop.setter, + ), + rule, + ruleName, + }); + }, + + // ✅ Good - dispatcher configured BEFORE (separate) + "dispatcher before separate with property": () => { + testDenoLint({ + code: createSeparateDispatcherCode( + createTestCode("class", propertyKey, true), + prop.setter, + true, + ), + rule, + ruleName, + }); + }, + + // ✅ Good - dispatcher configured AFTER (chained) + "dispatcher after chained with property": () => { + testDenoLint({ + code: createChainedDispatcherCode( + createTestCode("class", propertyKey, true), + prop.setter, + ), + rule, + ruleName, + }); + }, + + // ✅ Good - dispatcher configured AFTER (separate) + "dispatcher after separate with property": () => { + testDenoLint({ + code: createSeparateDispatcherCode( + createTestCode("class", propertyKey, true), + prop.setter, + false, + ), + rule, + ruleName, + }); + }, + + // ✅ Good - object literal with property + "object literal with property": () => { + testDenoLint({ + code: createChainedDispatcherCode( + createTestCode("literal", propertyKey, true), + prop.setter, + ), + rule, + ruleName, + }); + }, + + // ❌ Bad - dispatcher configured, property missing + "dispatcher configured property missing": () => { + testDenoLint({ + code: createChainedDispatcherCode( + createTestCode("class", propertyKey, false), + prop.setter, + ), + rule, + ruleName, + expectedError, + }); + }, + + // ❌ Bad - dispatcher before (separate), property missing + "dispatcher before separate property missing": () => { + testDenoLint({ + code: createSeparateDispatcherCode( + createTestCode("class", propertyKey, false), + prop.setter, + true, + ), + rule, + ruleName, + expectedError, + }); + }, + + // ❌ Bad - dispatcher after (separate), property missing + "dispatcher after separate property missing": () => { + testDenoLint({ + code: createSeparateDispatcherCode( + createTestCode("class", propertyKey, false), + prop.setter, + false, + ), + rule, + ruleName, + expectedError, + }); + }, + + // ❌ Bad - object literal without property + "object literal without property": () => { + testDenoLint({ + code: createChainedDispatcherCode( + createTestCode("literal", propertyKey, false), + prop.setter, + ), + rule, + ruleName, + expectedError, + }); + }, + + // ❌ Bad - variable assignment without property + "variable assignment without property": () => { + testDenoLint({ + code: createChainedDispatcherCode( + `const actor = new Person({ + id: ctx.getActorUri(identifier), + name: "John Doe", + }); + return actor;`, + prop.setter, + ), + rule, + ruleName, + expectedError, + }); + }, + }; +} + +/** + * Creates required rule tests for inbox/sharedInbox properties + */ +export function createRequiredListenerRuleTests( + propertyKey: "inbox" | "sharedInbox", + config: TestConfig, +) { + const { rule, ruleName } = config; + const prop = properties[propertyKey]; + const expectedError = actorPropertyRequired(prop.name); + const isNested = propertyKey === "sharedInbox"; + + const createPropertyCode = (include: boolean) => { + if (!include) return ""; + if (isNested) return "endpoints: { sharedInbox: ctx.getInboxUri() },"; + return "inbox: ctx.getInboxUri(identifier),"; + }; + + const createActorCode = ( + type: "class" | "literal", + includeProperty: boolean, + ) => { + const propCode = createPropertyCode(includeProperty); + if (type === "class") { + return `return new Person({ + id: ctx.getActorUri(identifier), + ${propCode} + name: "John Doe", + });`; + } + return `return { + id: ctx.getActorUri(identifier), + ${propCode} + name: "John Doe", + };`; + }; + + return { + // ✅ Good - non-Federation object + "non-federation object": () => { + testDenoLint({ + code: createDispatcherCode(createActorCode("class", false)), + rule, + ruleName, + federationSetup: ` + const federation = { setActorDispatcher: () => {} }; + `, + }); + }, + + // ✅ Good - listeners NOT configured + "listeners not configured": () => { + testDenoLint({ + code: createDispatcherCode(createActorCode("class", false)), + rule, + ruleName, + }); + }, + + // ✅ Good - listeners configured BEFORE (chained) + "listeners before chained with property": () => { + testDenoLint({ + code: createChainedDispatcherCode( + createActorCode("class", true), + "setInboxListeners", + ), + rule, + ruleName, + }); + }, + + // ✅ Good - listeners configured BEFORE (separate) + "listeners before separate with property": () => { + testDenoLint({ + code: createSeparateDispatcherCode( + createActorCode("class", true), + "setInboxListeners", + true, + ), + rule, + ruleName, + }); + }, + + // ✅ Good - listeners configured AFTER (chained) + "listeners after chained with property": () => { + testDenoLint({ + code: createChainedDispatcherCode( + createActorCode("class", true), + "setInboxListeners", + ), + rule, + ruleName, + }); + }, + + // ✅ Good - listeners configured AFTER (separate) + "listeners after separate with property": () => { + testDenoLint({ + code: createSeparateDispatcherCode( + createActorCode("class", true), + "setInboxListeners", + false, + ), + rule, + ruleName, + }); + }, + + // ✅ Good - object literal with property + "object literal with property": () => { + testDenoLint({ + code: createChainedDispatcherCode( + createActorCode("literal", true), + "setInboxListeners", + ), + rule, + ruleName, + }); + }, + + // ❌ Bad - listeners configured, property missing + "listeners configured property missing": () => { + testDenoLint({ + code: createChainedDispatcherCode( + createActorCode("class", false), + "setInboxListeners", + ), + rule, + ruleName, + expectedError, + }); + }, + + // ❌ Bad - listeners before (separate), property missing + "listeners before separate property missing": () => { + testDenoLint({ + code: createSeparateDispatcherCode( + createActorCode("class", false), + "setInboxListeners", + true, + ), + rule, + ruleName, + expectedError, + }); + }, + + // ❌ Bad - listeners after (separate), property missing + "listeners after separate property missing": () => { + testDenoLint({ + code: createSeparateDispatcherCode( + createActorCode("class", false), + "setInboxListeners", + false, + ), + rule, + ruleName, + expectedError, + }); + }, + + // ❌ Bad - object literal without property + "object literal without property": () => { + testDenoLint({ + code: createChainedDispatcherCode( + createActorCode("literal", false), + "setInboxListeners", + ), + rule, + ruleName, + expectedError, + }); + }, + + // ❌ Bad - variable assignment without property + "variable assignment without property": () => { + testDenoLint({ + code: createChainedDispatcherCode( + `const actor = new Person({ + id: ctx.getActorUri(identifier), + name: "John Doe", + }); + return actor;`, + "setInboxListeners", + ), + rule, + ruleName, + expectedError, + }); + }, + }; +} + +/** + * Creates required rule tests for actor id property (no dispatcher needed) + */ +export function createIdRequiredRuleTests(config: TestConfig) { + const { rule, ruleName } = config; + const expectedError = actorPropertyRequired(properties.id.name); + + return { + // ✅ Good - non-Federation object + "non-federation object": () => { + testDenoLint({ + code: createDispatcherCode(`return new Person({ name: "John Doe" });`), + rule, + ruleName, + federationSetup: ` + const federation = { setActorDispatcher: () => {} }; + `, + }); + }, + + // ✅ Good - with id property (any value) + "with id property any value": () => { + testDenoLint({ + code: createDispatcherCode(`return new Person({ + id: "https://example.com/users/123", + name: "John Doe", + });`), + rule, + ruleName, + }); + }, + + // ✅ Good - with id property using ctx.getActorUri() + "with id property using getActorUri": () => { + testDenoLint({ + code: createDispatcherCode(`return new Person({ + id: ctx.getActorUri(identifier), + name: "John Doe", + });`), + rule, + ruleName, + }); + }, + + // ✅ Good - object literal with id + "object literal with id": () => { + testDenoLint({ + code: createDispatcherCode(`return { + id: "https://example.com/users/123", + name: "John Doe", + };`), + rule, + ruleName, + }); + }, + + // ✅ Good - BlockStatement with id + "block statement with id": () => { + testDenoLint({ + code: createDispatcherCode(`const name = "John Doe"; + return new Person({ + id: ctx.getActorUri(identifier), + name, + });`), + rule, + ruleName, + }); + }, + + // ❌ Bad - without id property + "without id property": () => { + testDenoLint({ + code: createDispatcherCode(`return new Person({ name: "John Doe" });`), + rule, + ruleName, + expectedError, + }); + }, + + // ❌ Bad - returning empty object + "returning empty object": () => { + testDenoLint({ + code: createDispatcherCode(`return new Person({});`), + rule, + ruleName, + expectedError, + }); + }, + + // ✅ Good - multiple properties including id + "multiple properties including id": () => { + testDenoLint({ + code: createDispatcherCode(`return new Person({ + id: ctx.getActorUri(identifier), + name: "John Doe", + inbox: ctx.getInboxUri(identifier), + outbox: ctx.getOutboxUri(identifier), + });`), + rule, + ruleName, + }); + }, + + // ❌ Bad - variable assignment without id + "variable assignment without id": () => { + testDenoLint({ + code: createDispatcherCode( + `const actor = new Person({ name: "John Doe" }); + return actor;`, + ), + rule, + ruleName, + expectedError, + }); + }, + + // ❌ Bad - object literal without id + "object literal without id": () => { + testDenoLint({ + code: createDispatcherCode(`return { name: "John Doe" };`), + rule, + ruleName, + expectedError, + }); + }, + }; +} + +/** + * Creates required rule tests for key-related properties (publicKey, assertionMethod) + */ +export function createKeyRequiredRuleTests( + propertyName: "publicKey" | "assertionMethod", + config: TestConfig, +) { + const { rule, ruleName } = config; + const expectedError = actorKeyPropertyRequired(propertyName); + + const createActorWithKey = (includeProperty: boolean) => { + const propCode = includeProperty + ? `${propertyName}: ctx.getActorKeyPairs(identifier),` + : ""; + return `return new Person({ + id: ctx.getActorUri(identifier), + ${propCode} + name: "John Doe", + });`; + }; + + return { + // ✅ Good - non-Federation object + "non-federation object": () => { + testDenoLint({ + code: createDispatcherCode(`return new Person({ name: "John Doe" });`), + rule, + ruleName, + federationSetup: ` + const federation = { setActorDispatcher: () => {} }; + `, + }); + }, + + // ✅ Good - key pairs dispatcher NOT configured + "key pairs dispatcher not configured": () => { + testDenoLint({ + code: createDispatcherCode(createActorWithKey(false)), + rule, + ruleName, + }); + }, + + // ✅ Good - key pairs dispatcher configured BEFORE (separate) + "key pairs before separate with property": () => { + testDenoLint({ + code: createSeparateDispatcherCode( + createActorWithKey(true), + "setKeyPairsDispatcher", + true, + ), + rule, + ruleName, + }); + }, + + // ✅ Good - key pairs dispatcher configured BEFORE (chained) + "key pairs before chained with property": () => { + testDenoLint({ + code: createChainedDispatcherCode( + createActorWithKey(true), + "setKeyPairsDispatcher", + ), + rule, + ruleName, + }); + }, + + // ✅ Good - key pairs dispatcher configured AFTER (separate) + "key pairs after separate with property": () => { + testDenoLint({ + code: createSeparateDispatcherCode( + createActorWithKey(true), + "setKeyPairsDispatcher", + false, + ), + rule, + ruleName, + }); + }, + + // ✅ Good - key pairs dispatcher configured AFTER (chained) + "key pairs after chained with property": () => { + testDenoLint({ + code: createChainedDispatcherCode( + createActorWithKey(true), + "setKeyPairsDispatcher", + ), + rule, + ruleName, + }); + }, + + // ✅ Good - object literal with property + "object literal with property": () => { + testDenoLint({ + code: createSeparateDispatcherCode( + `return { + id: ctx.getActorUri(identifier), + ${propertyName}: ctx.getActorKeyPairs(identifier), + name: "John Doe", + };`, + "setKeyPairsDispatcher", + true, + ), + rule, + ruleName, + }); + }, + + // ❌ Bad - key pairs dispatcher configured BEFORE (separate), property missing + "key pairs before separate property missing": () => { + testDenoLint({ + code: createSeparateDispatcherCode( + createActorWithKey(false), + "setKeyPairsDispatcher", + true, + ), + rule, + ruleName, + expectedError, + }); + }, + + // ❌ Bad - key pairs dispatcher configured BEFORE (chained), property missing + "key pairs before chained property missing": () => { + testDenoLint({ + code: createChainedDispatcherCode( + createActorWithKey(false), + "setKeyPairsDispatcher", + ), + rule, + ruleName, + expectedError, + }); + }, + + // ❌ Bad - key pairs dispatcher configured AFTER (separate), property missing + "key pairs after separate property missing": () => { + testDenoLint({ + code: createSeparateDispatcherCode( + createActorWithKey(false), + "setKeyPairsDispatcher", + false, + ), + rule, + ruleName, + expectedError, + }); + }, + + // ❌ Bad - key pairs dispatcher configured AFTER (chained), property missing + "key pairs after chained property missing": () => { + testDenoLint({ + code: createChainedDispatcherCode( + createActorWithKey(false), + "setKeyPairsDispatcher", + ), + rule, + ruleName, + expectedError, + }); + }, + + // ❌ Bad - object literal without property + "object literal without property": () => { + testDenoLint({ + code: createSeparateDispatcherCode( + `return { + id: ctx.getActorUri(identifier), + name: "John Doe", + };`, + "setKeyPairsDispatcher", + true, + ), + rule, + ruleName, + expectedError, + }); + }, + }; +} + +// ============================================================================= +// Mismatch Rule Tests +// ============================================================================= + +/** + * Creates mismatch rule tests for standard properties + */ +export function createMismatchRuleTests( + propertyKey: PropertyKey, + config: TestConfig, +) { + const { rule, ruleName } = config; + const prop = properties[propertyKey]; + const isNested = prop.name.includes("."); + + // Build the expected method call string + const expectedMethodCall = isNested + ? `ctx.${prop.getter}()` + : `ctx.${prop.getter}(identifier)`; + const expectedError = actorPropertyMismatch(prop.name, expectedMethodCall); + + // Find a wrong getter for testing + const wrongGetters = Object.values(properties) + .filter((p) => p.getter !== prop.getter) + .map((p) => p.getter); + const wrongGetter = wrongGetters[0] || "getWrongUri"; + + const createPropertyCode = (getter: string) => { + if (isNested) { + // endpoints.sharedInbox + return `endpoints: { sharedInbox: ctx.${getter}() },`; + } + return `${prop.name}: ctx.${getter}(identifier),`; + }; + + const createActorCode = (getter: string) => + `return new Person({ + id: ctx.getActorUri(identifier), + ${createPropertyCode(getter)} + name: "John Doe", + });`; + + return { + // ✅ Good - non-Federation object + "non-federation object": () => { + testDenoLint({ + code: createDispatcherCode(createActorCode(wrongGetter)), + rule, + ruleName, + federationSetup: ` + const federation = { setActorDispatcher: () => {} }; + `, + }); + }, + + // ✅ Good - correct getter used + "correct getter used": () => { + testDenoLint({ + code: createDispatcherCode(createActorCode(prop.getter)), + rule, + ruleName, + }); + }, + + // ❌ Bad - wrong getter used + "wrong getter used": () => { + testDenoLint({ + code: createDispatcherCode(createActorCode(wrongGetter)), + rule, + ruleName, + expectedError, + }); + }, + + // ❌ Bad - wrong method in object literal + "wrong method in object literal": () => { + const propCode = isNested + ? `endpoints: { sharedInbox: ctx.${wrongGetter}() },` + : `${prop.name}: ctx.${wrongGetter}(identifier),`; + testDenoLint({ + code: createDispatcherCode(`return { + id: ctx.getActorUri(identifier), + ${propCode} + name: "John Doe", + };`), + rule, + ruleName, + expectedError, + }); + }, + + // ❌ Bad - wrong identifier + "wrong identifier": () => { + const propCode = isNested + ? `endpoints: { sharedInbox: wrongContext.${prop.getter}() },` + : `${prop.name}: wrongContext.${prop.getter}(identifier),`; + testDenoLint({ + code: createDispatcherCode(`return new Person({ + id: ctx.getActorUri(identifier), + ${propCode} + name: "John Doe", + });`), + rule, + ruleName, + expectedError, + }); + }, + + // ✅ Good - property not present (no error) + "property not present": () => { + testDenoLint({ + code: createDispatcherCode(`return new Person({ + id: ctx.getActorUri(identifier), + name: "John Doe", + });`), + rule, + ruleName, + }); + }, + + // ✅ Good - object literal with correct getter + "object literal with correct getter": () => { + const propCode = isNested + ? `endpoints: { sharedInbox: ctx.${prop.getter}() },` + : `${prop.name}: ctx.${prop.getter}(identifier),`; + testDenoLint({ + code: createDispatcherCode(`return { + id: ctx.getActorUri(identifier), + ${propCode} + name: "John Doe", + };`), + rule, + ruleName, + }); + }, + }; +} + +/** + * Creates mismatch rule tests for id property + */ +export function createIdMismatchRuleTests(config: TestConfig) { + const { rule, ruleName } = config; + const expectedError = actorPropertyMismatch( + properties.id.name, + properties.id.getter, + ); + + return { + // ✅ Good - non-Federation object + "non-federation object": () => { + testDenoLint({ + code: createDispatcherCode( + `return new Person({ id: ctx.getFollowingUri(identifier) });`, + ), + rule, + ruleName, + federationSetup: ` + const federation = { setActorDispatcher: () => {} }; + `, + }); + }, + + // ✅ Good - correct getter used + "correct getter used": () => { + testDenoLint({ + code: createDispatcherCode( + `return new Person({ id: ctx.getActorUri(identifier) });`, + ), + rule, + ruleName, + }); + }, + + // ✅ Good - literal string id + "literal string id": () => { + testDenoLint({ + code: createDispatcherCode( + `return new Person({ id: "https://example.com/users/123" });`, + ), + rule, + ruleName, + }); + }, + + // ✅ Good - new URL as id + "new URL as id": () => { + testDenoLint({ + code: createDispatcherCode( + `return new Person({ id: new URL("https://example.com/users/123") });`, + ), + rule, + ruleName, + }); + }, + + // ❌ Bad - wrong getter used + "wrong getter used": () => { + testDenoLint({ + code: createDispatcherCode( + `return new Person({ id: ctx.getFollowingUri(identifier) });`, + ), + rule, + ruleName, + expectedError, + }); + }, + + // ❌ Bad - wrong method in object literal + "wrong method in object literal": () => { + testDenoLint({ + code: createDispatcherCode( + `return { id: ctx.getInboxUri(identifier) };`, + ), + rule, + ruleName, + expectedError, + }); + }, + + // ❌ Bad - wrong identifier + "wrong identifier": () => { + testDenoLint({ + code: createDispatcherCode( + `return new Person({ id: wrongContext.getActorUri(identifier) });`, + ), + rule, + ruleName, + expectedError, + }); + }, + }; +} + +// ============================================================================= +// Edge Case Tests +// ============================================================================= + +/** + * Creates common edge case tests for required rules + */ +export function createRequiredEdgeCaseTests( + propertyKey: PropertyKey, + config: TestConfig, + dispatcherMethod: string, +) { + const { rule, ruleName } = config; + const prop = properties[propertyKey]; + const expectedError = actorPropertyRequired(prop.name); + const isNested = prop.name.includes("."); + + const createPropertyCode = () => { + if (isNested) return "endpoints: { sharedInbox: ctx.getInboxUri() },"; + return `${prop.name}: ctx.${prop.getter}(identifier),`; + }; + + return { + // ✅ Ternary with property in both branches + "ternary with property in both branches": () => { + const propCode = createPropertyCode(); + testDenoLint({ + code: createChainedDispatcherCode( + `return condition + ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" });`, + dispatcherMethod, + ), + rule, + ruleName, + }); + }, + + // ❌ Ternary missing property in consequent + "ternary missing property in consequent": () => { + const propCode = createPropertyCode(); + testDenoLint({ + code: createChainedDispatcherCode( + `return condition + ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" });`, + dispatcherMethod, + ), + rule, + ruleName, + expectedError, + }); + }, + + // ❌ Ternary missing property in alternate + "ternary missing property in alternate": () => { + const propCode = createPropertyCode(); + testDenoLint({ + code: createChainedDispatcherCode( + `return condition + ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), name: "B" });`, + dispatcherMethod, + ), + rule, + ruleName, + expectedError, + }); + }, + + // ❌ Ternary missing property in both + "ternary missing property in both branches": () => { + testDenoLint({ + code: createChainedDispatcherCode( + `return condition + ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), name: "B" });`, + dispatcherMethod, + ), + rule, + ruleName, + expectedError, + }); + }, + + // ✅ Nested ternary with property + "nested ternary with property": () => { + const propCode = createPropertyCode(); + testDenoLint({ + code: createChainedDispatcherCode( + `return condition1 + ? (condition2 + ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" })) + : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" });`, + dispatcherMethod, + ), + rule, + ruleName, + }); + }, + }; +} + +/** + * Creates common edge case tests for mismatch rules + */ +export function createMismatchEdgeCaseTests( + propertyKey: PropertyKey, + config: TestConfig, +) { + const { rule, ruleName } = config; + const prop = properties[propertyKey]; + const isNested = prop.name.includes("."); + + // Build the expected method call string + const expectedMethodCall = isNested + ? `ctx.${prop.getter}()` + : `ctx.${prop.getter}(identifier)`; + const expectedError = actorPropertyMismatch(prop.name, expectedMethodCall); + + // Find a wrong getter for testing + const wrongGetters = Object.values(properties) + .filter((p) => p.getter !== prop.getter) + .map((p) => p.getter); + const wrongGetter = wrongGetters[0] || "getWrongUri"; + + const createPropertyCode = (getter: string) => { + if (isNested) return `endpoints: { sharedInbox: ctx.${getter}() },`; + return `${prop.name}: ctx.${getter}(identifier),`; + }; + + return { + // ✅ Ternary with correct getter in both branches + "ternary with correct getter in both branches": () => { + const propCode = createPropertyCode(prop.getter); + testDenoLint({ + code: createDispatcherCode( + `return condition + ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" });`, + ), + rule, + ruleName, + }); + }, + + // ❌ Ternary with wrong getter in consequent + "ternary with wrong getter in consequent": () => { + const correctPropCode = createPropertyCode(prop.getter); + const wrongPropCode = createPropertyCode(wrongGetter); + testDenoLint({ + code: createDispatcherCode( + `return condition + ? new Person({ id: ctx.getActorUri(identifier), ${wrongPropCode} name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), ${correctPropCode} name: "B" });`, + ), + rule, + ruleName, + expectedError, + }); + }, + + // ❌ Ternary with wrong getter in alternate + "ternary with wrong getter in alternate": () => { + const correctPropCode = createPropertyCode(prop.getter); + const wrongPropCode = createPropertyCode(wrongGetter); + testDenoLint({ + code: createDispatcherCode( + `return condition + ? new Person({ id: ctx.getActorUri(identifier), ${correctPropCode} name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), ${wrongPropCode} name: "B" });`, + ), + rule, + ruleName, + expectedError, + }); + }, + + // ❌ Ternary with wrong getter in both + "ternary with wrong getter in both branches": () => { + const wrongPropCode = createPropertyCode(wrongGetter); + testDenoLint({ + code: createDispatcherCode( + `return condition + ? new Person({ id: ctx.getActorUri(identifier), ${wrongPropCode} name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), ${wrongPropCode} name: "B" });`, + ), + rule, + ruleName, + expectedError, + }); + }, + + // ✅ Nested ternary with correct getter + "nested ternary with correct getter": () => { + const propCode = createPropertyCode(prop.getter); + testDenoLint({ + code: createDispatcherCode( + `return condition1 + ? (condition2 + ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" })) + : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" });`, + ), + rule, + ruleName, + }); + }, + }; +} + +/** + * Creates edge case tests for id required rule + */ +export function createIdRequiredEdgeCaseTests(config: TestConfig) { + const { rule, ruleName } = config; + const expectedError = actorPropertyRequired(properties.id.name); + + return { + // ✅ Ternary with id in both branches + "ternary with id in both branches": () => { + testDenoLint({ + code: createDispatcherCode( + `return condition + ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), name: "B" });`, + ), + rule, + ruleName, + }); + }, + + // ❌ Ternary missing id in consequent + "ternary missing id in consequent": () => { + testDenoLint({ + code: createDispatcherCode( + `return condition + ? new Person({ name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), name: "B" });`, + ), + rule, + ruleName, + expectedError, + }); + }, + + // ❌ Ternary missing id in alternate + "ternary missing id in alternate": () => { + testDenoLint({ + code: createDispatcherCode( + `return condition + ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) + : new Person({ name: "B" });`, + ), + rule, + ruleName, + expectedError, + }); + }, + + // ❌ Ternary missing id in both + "ternary missing id in both branches": () => { + testDenoLint({ + code: createDispatcherCode( + `return condition + ? new Person({ name: "A" }) + : new Person({ name: "B" });`, + ), + rule, + ruleName, + expectedError, + }); + }, + + // ✅ Nested ternary with id + "nested ternary with id": () => { + testDenoLint({ + code: createDispatcherCode( + `return condition1 + ? (condition2 + ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), name: "B" })) + : new Person({ id: ctx.getActorUri(identifier), name: "C" });`, + ), + rule, + ruleName, + }); + }, + }; +} + +/** + * Creates edge case tests for id mismatch rule + */ +export function createIdMismatchEdgeCaseTests(config: TestConfig) { + const { rule, ruleName } = config; + const expectedError = actorPropertyMismatch( + properties.id.name, + properties.id.getter, + ); + + return { + // ✅ Ternary with correct getter in both branches + "ternary with correct getter in both branches": () => { + testDenoLint({ + code: createDispatcherCode( + `return condition + ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), name: "B" });`, + ), + rule, + ruleName, + }); + }, + + // ❌ Ternary with wrong getter in consequent + "ternary with wrong getter in consequent": () => { + testDenoLint({ + code: createDispatcherCode( + `return condition + ? new Person({ id: ctx.getFollowingUri(identifier), name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), name: "B" });`, + ), + rule, + ruleName, + expectedError, + }); + }, + + // ❌ Ternary with wrong getter in alternate + "ternary with wrong getter in alternate": () => { + testDenoLint({ + code: createDispatcherCode( + `return condition + ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) + : new Person({ id: ctx.getFollowingUri(identifier), name: "B" });`, + ), + rule, + ruleName, + expectedError, + }); + }, + + // ❌ Ternary with wrong getter in both + "ternary with wrong getter in both branches": () => { + testDenoLint({ + code: createDispatcherCode( + `return condition + ? new Person({ id: ctx.getFollowingUri(identifier), name: "A" }) + : new Person({ id: ctx.getFollowingUri(identifier), name: "B" });`, + ), + rule, + ruleName, + expectedError, + }); + }, + + // ✅ Nested ternary with correct getter + "nested ternary with correct getter": () => { + testDenoLint({ + code: createDispatcherCode( + `return condition1 + ? (condition2 + ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), name: "B" })) + : new Person({ id: ctx.getActorUri(identifier), name: "C" });`, + ), + rule, + ruleName, + }); + }, + }; +} + +/** + * Creates edge case tests for key required rules (publicKey, assertionMethod) + */ +export function createKeyRequiredEdgeCaseTests( + propertyName: "publicKey" | "assertionMethod", + config: TestConfig, +) { + const { rule, ruleName } = config; + const expectedError = actorKeyPropertyRequired(propertyName); + + const createPropertyCode = () => + `${propertyName}: ctx.getActorKeyPairs(identifier),`; + + return { + // ✅ Ternary with property in both branches + "ternary with property in both branches": () => { + const propCode = createPropertyCode(); + testDenoLint({ + code: createChainedDispatcherCode( + `return condition + ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" });`, + "setKeyPairsDispatcher", + ), + rule, + ruleName, + }); + }, + + // ❌ Ternary missing property in consequent + "ternary missing property in consequent": () => { + const propCode = createPropertyCode(); + testDenoLint({ + code: createChainedDispatcherCode( + `return condition + ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" });`, + "setKeyPairsDispatcher", + ), + rule, + ruleName, + expectedError, + }); + }, + + // ❌ Ternary missing property in alternate + "ternary missing property in alternate": () => { + const propCode = createPropertyCode(); + testDenoLint({ + code: createChainedDispatcherCode( + `return condition + ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), name: "B" });`, + "setKeyPairsDispatcher", + ), + rule, + ruleName, + expectedError, + }); + }, + + // ❌ Ternary missing property in both + "ternary missing property in both branches": () => { + testDenoLint({ + code: createChainedDispatcherCode( + `return condition + ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), name: "B" });`, + "setKeyPairsDispatcher", + ), + rule, + ruleName, + expectedError, + }); + }, + + // ✅ Nested ternary with property + "nested ternary with property": () => { + const propCode = createPropertyCode(); + testDenoLint({ + code: createChainedDispatcherCode( + `return condition1 + ? (condition2 + ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" })) + : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" });`, + "setKeyPairsDispatcher", + ), + rule, + ruleName, + }); + }, + }; +} + +// ============================================================================= +// Test Runner Utilities +// ============================================================================= + +/** + * Runs all tests from a test object + */ +export function runTests( + tests: Record void>, + ruleName: string, + prefix: string, +) { + const { test } = require("node:test"); + for (const [name, testFn] of Object.entries(tests)) { + test(`${ruleName}: ${prefix} - ${name}`, testFn); + } +} diff --git a/packages/lint/src/lib/tracker.ts b/packages/lint/src/lib/tracker.ts index 9b4a83c23..88082f955 100644 --- a/packages/lint/src/lib/tracker.ts +++ b/packages/lint/src/lib/tracker.ts @@ -4,24 +4,35 @@ export function trackFederationVariables() { const federationVariables = new Set(); + const isFederationObject = (obj: Deno.lint.Expression): boolean => { + switch (obj.type) { + case "Identifier": + return federationVariables.has(obj.name); + case "CallExpression": + // Check if it's a direct createFederation call + if (isCreateFederation(obj)) return true; + // Check if it's a chained method call on a federation object + if (obj.callee.type === "MemberExpression") { + return isFederationObject(obj.callee.object); + } + return false; + case "MemberExpression": + return isFederationObject(obj.object); + } + return false; + }; + return { - VariableDeclarator(node: unknown) { - if (typeof node !== "object" || node === null) return; - const n = node as Record; - const init = n.init as Record | null | undefined; - const id = n.id as Record; + VariableDeclarator(node: Deno.lint.VariableDeclarator) { + const init = node.init; + const id = node.id; - if ( - init?.type === "CallExpression" && - typeof init.callee === "object" && init.callee !== null - ) { - const callee = init.callee as Record; - if (callee.type === "Identifier" && typeof callee.name === "string") { - if (/^create(Federation|FederationBuilder)$/i.test(callee.name)) { - if (id.type === "Identifier" && typeof id.name === "string") { - federationVariables.add(id.name); - } - } + if (init?.type === "CallExpression") { + if ( + isCreateFederation(init) && + id.type === "Identifier" + ) { + federationVariables.add(id.name); } } }, @@ -30,22 +41,12 @@ export function trackFederationVariables() { return federationVariables.has(name); }, - isFederationObject(obj: unknown): boolean { - if (typeof obj !== "object" || obj === null) return false; - const o = obj as Record; - - if (o.type === "Identifier" && typeof o.name === "string") { - return federationVariables.has(o.name); - } else if ( - o.type === "CallExpression" && - typeof o.callee === "object" && o.callee !== null - ) { - const callee = o.callee as Record; - if (callee.type === "Identifier" && typeof callee.name === "string") { - return /^create(Federation|FederationBuilder)$/i.test(callee.name); - } - } - return false; - }, + isFederationObject, }; } + +const isCreateFederation = ( + node: Deno.lint.CallExpression, +): boolean => + node.callee.type === "Identifier" && + /^create(Federation|FederationBuilder)$/i.test(node.callee.name); diff --git a/packages/lint/src/lib/types.ts b/packages/lint/src/lib/types.ts new file mode 100644 index 000000000..d002d7b6c --- /dev/null +++ b/packages/lint/src/lib/types.ts @@ -0,0 +1,49 @@ +interface CallExpressionWithoutCallee { + type: "CallExpression"; + range: Deno.lint.Range; + optional: boolean; + typeArguments: Deno.lint.TSTypeParameterInstantiation | null; + arguments: Array; + parent: Deno.lint.Node; +} +export interface CallMemberExpression extends CallExpressionWithoutCallee { + callee: Deno.lint.MemberExpression; +} + +export interface CallMemberExpressionWithIdentified + extends CallExpressionWithoutCallee { + callee: Deno.lint.MemberExpression & { + property: Deno.lint.Identifier; + }; +} + +export type FunctionNode = + | Deno.lint.ArrowFunctionExpression + | Deno.lint.FunctionExpression; + +/** + * Configuration for property mismatch rules. + * These rules check if a property uses the correct `ctx.get*()` method. + */ +export interface MismatchRuleConfig { + /** Property path to check. Can be a single property name or an array for nested properties. */ + propertyPath: string; + /** Expected context method name (e.g., "getActorUri", "getInboxUri") */ + methodName: string; + /** Whether the method requires identifier parameter (default: true) */ + requiresIdentifier?: boolean; +} + +export type ASTNode = + & { "type": string } + & (Deno.lint.Node | Deno.lint.Parameter); + +/** + * Context for method call validation. + */ +export interface MethodCallContext { + ctxName: string; + idName: string; + methodName: string; + requiresIdentifier: boolean; +} diff --git a/packages/lint/src/lib/utils.ts b/packages/lint/src/lib/utils.ts new file mode 100644 index 000000000..33a88be93 --- /dev/null +++ b/packages/lint/src/lib/utils.ts @@ -0,0 +1,20 @@ +/** + * Returns the value of the key in the object. + */ +export function getProp( + key: T, +): (obj: S) => S[T]; +export function getProp( + key: T, +): (obj: S) => S[T] { + return (obj: S) => obj[key]; +} + +export const eq = (value: S) => (other: T): boolean => + value === other; + +export const getArticle = (word: string): string => + /^[aeiou]/i.test(word) ? "an" : "a"; + +export const endsWith = (suffix: string) => (str: string): boolean => + str.endsWith(suffix); diff --git a/packages/lint/src/mod.ts b/packages/lint/src/mod.ts index d4927588f..ecd7508ab 100644 --- a/packages/lint/src/mod.ts +++ b/packages/lint/src/mod.ts @@ -58,13 +58,9 @@ import actorSharedInboxPropertyMismatch, { import actorSharedInboxPropertyRequired, { ACTOR_SHARED_INBOX_PROPERTY_REQUIRED, } from "./rules/actor-shared-inbox-property-required.ts"; -import collectionFilteringNotImplemented, { +/* import collectionFilteringNotImplemented, { COLLECTION_FILTERING_NOT_IMPLEMENTED, -} from "./rules/collection-filtering-not-implemented.ts"; -import ed25519KeyRequired, { - ED25519_KEY_REQUIRED, -} from "./rules/ed25519-key-required.ts"; -import rsaKeyRequired, { RSA_KEY_REQUIRED } from "./rules/rsa-key-required.ts"; +} from "./rules/collection-filtering-not-implemented.ts"; */ const plugin: Deno.lint.Plugin = { name: "@fedify/lint", @@ -83,15 +79,13 @@ const plugin: Deno.lint.Plugin = { [ACTOR_FEATURED_PROPERTY_MISMATCH]: actorFeaturedPropertyMismatch, [ACTOR_FEATURED_TAGS_PROPERTY_REQUIRED]: actorFeaturedTagsPropertyRequired, [ACTOR_FEATURED_TAGS_PROPERTY_MISMATCH]: actorFeaturedTagsPropertyMismatch, - [COLLECTION_FILTERING_NOT_IMPLEMENTED]: collectionFilteringNotImplemented, [ACTOR_INBOX_PROPERTY_REQUIRED]: actorInboxPropertyRequired, [ACTOR_INBOX_PROPERTY_MISMATCH]: actorInboxPropertyMismatch, [ACTOR_SHARED_INBOX_PROPERTY_REQUIRED]: actorSharedInboxPropertyRequired, [ACTOR_SHARED_INBOX_PROPERTY_MISMATCH]: actorSharedInboxPropertyMismatch, [ACTOR_PUBLIC_KEY_REQUIRED]: actorPublicKeyRequired, [ACTOR_ASSERTION_METHOD_REQUIRED]: actorAssertionMethodRequired, - [RSA_KEY_REQUIRED]: rsaKeyRequired, - [ED25519_KEY_REQUIRED]: ed25519KeyRequired, + // [COLLECTION_FILTERING_NOT_IMPLEMENTED]: collectionFilteringNotImplemented, }, }; diff --git a/packages/lint/src/rules/actor-assertion-method-required.ts b/packages/lint/src/rules/actor-assertion-method-required.ts index dbd6cd420..a9a7bbe62 100644 --- a/packages/lint/src/rules/actor-assertion-method-required.ts +++ b/packages/lint/src/rules/actor-assertion-method-required.ts @@ -1,5 +1,13 @@ +import { actorKeyPropertyRequired } from "../lib/messages.ts"; +import { createRequiredRule } from "../lib/required-rule-factory.ts"; + export const ACTOR_ASSERTION_METHOD_REQUIRED = "actor-assertion-method-required"; -const actorAssertionMethodRequired: Deno.lint.Rule = {}; +const actorAssertionMethodRequired: Deno.lint.Rule = createRequiredRule({ + propertyName: "assertionMethod", + dispatcherMethod: "setKeyPairsDispatcher", + errorMessage: actorKeyPropertyRequired("assertionMethod"), +}); + export default actorAssertionMethodRequired; diff --git a/packages/lint/src/rules/actor-featured-property-mismatch.ts b/packages/lint/src/rules/actor-featured-property-mismatch.ts index 22d7d22b2..651a4d904 100644 --- a/packages/lint/src/rules/actor-featured-property-mismatch.ts +++ b/packages/lint/src/rules/actor-featured-property-mismatch.ts @@ -1,5 +1,12 @@ +import { properties } from "../lib/const.ts"; +import { createMismatchRule } from "../lib/mismatch-rule-factory.ts"; + export const ACTOR_FEATURED_PROPERTY_MISMATCH = - "actor-featured-property-mismatch"; + "actor-featured-property-mismatch" as const; + +const actorFeaturedPropertyMismatch: Deno.lint.Rule = createMismatchRule({ + propertyPath: properties.featured.name, + methodName: properties.featured.getter, +}); -const actorFeaturedPropertyMismatch: Deno.lint.Rule = {}; export default actorFeaturedPropertyMismatch; diff --git a/packages/lint/src/rules/actor-featured-property-required.ts b/packages/lint/src/rules/actor-featured-property-required.ts index e73685de1..30e686b49 100644 --- a/packages/lint/src/rules/actor-featured-property-required.ts +++ b/packages/lint/src/rules/actor-featured-property-required.ts @@ -1,5 +1,14 @@ +import { properties } from "../lib/const.ts"; +import { actorPropertyRequired } from "../lib/messages.ts"; +import { createRequiredRule } from "../lib/required-rule-factory.ts"; + export const ACTOR_FEATURED_PROPERTY_REQUIRED = "actor-featured-property-required"; -const actorFeaturedPropertyRequired: Deno.lint.Rule = {}; +const actorFeaturedPropertyRequired = createRequiredRule({ + propertyName: properties.featured.name, + dispatcherMethod: properties.featured.setter, + errorMessage: actorPropertyRequired(properties.featured.name), +}); + export default actorFeaturedPropertyRequired; diff --git a/packages/lint/src/rules/actor-featured-tags-property-mismatch.ts b/packages/lint/src/rules/actor-featured-tags-property-mismatch.ts index fa867f830..26b5afa6f 100644 --- a/packages/lint/src/rules/actor-featured-tags-property-mismatch.ts +++ b/packages/lint/src/rules/actor-featured-tags-property-mismatch.ts @@ -1,5 +1,12 @@ +import { properties } from "../lib/const.ts"; +import { createMismatchRule } from "../lib/mismatch-rule-factory.ts"; + export const ACTOR_FEATURED_TAGS_PROPERTY_MISMATCH = - "actor-featured-tags-property-mismatch"; + "actor-featured-tags-property-mismatch" as const; + +const actorFeaturedTagsPropertyMismatch: Deno.lint.Rule = createMismatchRule({ + propertyPath: properties.featuredTags.name, + methodName: properties.featuredTags.getter, +}); -const actorFeaturedTagsPropertyMismatch: Deno.lint.Rule = {}; export default actorFeaturedTagsPropertyMismatch; diff --git a/packages/lint/src/rules/actor-featured-tags-property-required.ts b/packages/lint/src/rules/actor-featured-tags-property-required.ts index 4972149a1..33c40db63 100644 --- a/packages/lint/src/rules/actor-featured-tags-property-required.ts +++ b/packages/lint/src/rules/actor-featured-tags-property-required.ts @@ -1,5 +1,14 @@ +import { properties } from "../lib/const.ts"; +import { actorPropertyRequired } from "../lib/messages.ts"; +import { createRequiredRule } from "../lib/required-rule-factory.ts"; + export const ACTOR_FEATURED_TAGS_PROPERTY_REQUIRED = "actor-featured-tags-property-required"; -const actorFeaturedTagsPropertyRequired: Deno.lint.Rule = {}; +const actorFeaturedTagsPropertyRequired = createRequiredRule({ + propertyName: properties.featuredTags.name, + dispatcherMethod: properties.featuredTags.setter, + errorMessage: actorPropertyRequired(properties.featuredTags.name), +}); + export default actorFeaturedTagsPropertyRequired; diff --git a/packages/lint/src/rules/actor-followers-property-mismatch.ts b/packages/lint/src/rules/actor-followers-property-mismatch.ts index c71684dbf..2f78bebaf 100644 --- a/packages/lint/src/rules/actor-followers-property-mismatch.ts +++ b/packages/lint/src/rules/actor-followers-property-mismatch.ts @@ -1,5 +1,12 @@ +import { properties } from "../lib/const.ts"; +import { createMismatchRule } from "../lib/mismatch-rule-factory.ts"; + export const ACTOR_FOLLOWERS_PROPERTY_MISMATCH = - "actor-followers-property-mismatch"; + "actor-followers-property-mismatch" as const; + +const actorFollowersPropertyMismatch: Deno.lint.Rule = createMismatchRule({ + propertyPath: properties.followers.name, + methodName: properties.followers.getter, +}); -const actorFollowersPropertyMismatch: Deno.lint.Rule = {}; export default actorFollowersPropertyMismatch; diff --git a/packages/lint/src/rules/actor-followers-property-required.ts b/packages/lint/src/rules/actor-followers-property-required.ts index 55da56005..92c24aa73 100644 --- a/packages/lint/src/rules/actor-followers-property-required.ts +++ b/packages/lint/src/rules/actor-followers-property-required.ts @@ -1,5 +1,14 @@ +import { properties } from "../lib/const.ts"; +import { actorPropertyRequired } from "../lib/messages.ts"; +import { createRequiredRule } from "../lib/required-rule-factory.ts"; + export const ACTOR_FOLLOWERS_PROPERTY_REQUIRED = "actor-followers-property-required"; -const actorFollowersPropertyRequired: Deno.lint.Rule = {}; +const actorFollowersPropertyRequired = createRequiredRule({ + propertyName: properties.followers.name, + dispatcherMethod: properties.followers.setter, + errorMessage: actorPropertyRequired(properties.followers.name), +}); + export default actorFollowersPropertyRequired; diff --git a/packages/lint/src/rules/actor-following-property-mismatch.ts b/packages/lint/src/rules/actor-following-property-mismatch.ts index 1c88a5585..671290228 100644 --- a/packages/lint/src/rules/actor-following-property-mismatch.ts +++ b/packages/lint/src/rules/actor-following-property-mismatch.ts @@ -1,5 +1,12 @@ +import { properties } from "../lib/const.ts"; +import { createMismatchRule } from "../lib/mismatch-rule-factory.ts"; + export const ACTOR_FOLLOWING_PROPERTY_MISMATCH = - "actor-following-property-mismatch"; + "actor-following-property-mismatch" as const; + +const actorFollowingPropertyMismatch: Deno.lint.Rule = createMismatchRule({ + propertyPath: properties.following.name, + methodName: properties.following.getter, +}); -const actorFollowingPropertyMismatch: Deno.lint.Rule = {}; export default actorFollowingPropertyMismatch; diff --git a/packages/lint/src/rules/actor-following-property-required.ts b/packages/lint/src/rules/actor-following-property-required.ts index 7916b8f5e..fd01522ac 100644 --- a/packages/lint/src/rules/actor-following-property-required.ts +++ b/packages/lint/src/rules/actor-following-property-required.ts @@ -1,5 +1,14 @@ +import { properties } from "../lib/const.ts"; +import { actorPropertyRequired } from "../lib/messages.ts"; +import { createRequiredRule } from "../lib/required-rule-factory.ts"; + export const ACTOR_FOLLOWING_PROPERTY_REQUIRED = "actor-following-property-required"; -const actorFollowingPropertyRequired: Deno.lint.Rule = {}; +const actorFollowingPropertyRequired = createRequiredRule({ + propertyName: properties.following.name, + dispatcherMethod: properties.following.setter, + errorMessage: actorPropertyRequired(properties.following.name), +}); + export default actorFollowingPropertyRequired; diff --git a/packages/lint/src/rules/actor-id-mismatch.ts b/packages/lint/src/rules/actor-id-mismatch.ts index 3cc91fbb7..5ea6a5d7e 100644 --- a/packages/lint/src/rules/actor-id-mismatch.ts +++ b/packages/lint/src/rules/actor-id-mismatch.ts @@ -1,171 +1,18 @@ -import { filter, isNil, isObject, pipe, some } from "@fxts/core"; -import { trackFederationVariables } from "../lib/tracker.ts"; +import { properties } from "../lib/const.ts"; +import { createMismatchRule } from "../lib/mismatch-rule-factory.ts"; export const ACTOR_ID_MISMATCH = "actor-id-mismatch" as const; -const actorIdMismatch: Deno.lint.Rule = { - create(context) { - const tracker = trackFederationVariables(); - - return { - VariableDeclarator: tracker.VariableDeclarator, - - CallExpression(node) { - if ( - node.callee.type === "MemberExpression" && - node.callee.property.type === "Identifier" && - node.callee.property.name === "setActorDispatcher" && - node.arguments.length >= 2 - ) { - if (!tracker.isFederationObject(node.callee.object)) return; - - const dispatcherArg = node.arguments[1]; - - // Analyze dispatcher function - if ( - dispatcherArg.type === "ArrowFunctionExpression" || - dispatcherArg.type === "FunctionExpression" - ) { - // Extract function parameter names (context, identifier) - const params = dispatcherArg.params; - if (params.length < 2) return; - - const contextParam = params[0].type === "Identifier" - ? params[0].name - : null; - const identifierParam = params[1].type === "Identifier" - ? params[1].name - : null; - - if (!contextParam || !identifierParam) return; - - // Check if id property matches getActorUri call - const hasCorrectId = checkIdMatchesGetActorUri( - dispatcherArg.body, - contextParam, - identifierParam, - ); - - if (!hasCorrectId) { - context.report({ - node: dispatcherArg, - message: - `Actor's \`id\` property must match \`${contextParam}.getActorUri(${identifierParam})\`. Ensure you're using the correct context method.`, - }); - } - } - } - }, - }; - }, -}; +/** + * Lint rule that checks if the actor's `id` property uses the correct + * `ctx.getActorUri(identifier)` method call. + * + * This is a `*-mismatch` rule that validates the VALUE of the property, + * not just its existence. For checking property existence, use `actor-id-required`. + */ +const actorIdMismatch = createMismatchRule({ + propertyPath: properties.id.name, + methodName: properties.id.getter, +}); export default actorIdMismatch; - -// Declare isGetActorUriCall first (used in hasCorrectIdProperty) -function isGetActorUriCall( - node: unknown, - ctxName: string, - idName: string, -): boolean { - if (!isObject(node) || isNil(node)) return false; - const n = node as Record; - if (n.type !== "CallExpression") return false; - - const callee = n.callee as Record; - if (callee.type !== "MemberExpression") return false; - - const object = callee.object as Record; - const property = callee.property as Record; - const args = n.arguments as unknown[]; - - return object.type === "Identifier" && - object.name === ctxName && - property.type === "Identifier" && - property.name === "getActorUri" && - Array.isArray(args) && - args.length === 1 && - pipe( - args[0] as Record, - (arg) => arg.type === "Identifier" && arg.name === idName, - ); -} - -function hasCorrectIdProperty( - ctxName: string, - idName: string, - prop: unknown, -): boolean { - if (!isObject(prop) || isNil(prop)) return false; - const p = prop as Record; - const key = p.key as Record; - return p.type === "Property" && - key.type === "Identifier" && - key.name === "id" && - isGetActorUriCall(p.value, ctxName, idName); -} - -function checkObjectExpression( - obj: Record, - ctxName: string, - idName: string, -): boolean { - if (!Array.isArray(obj.properties)) return false; - return pipe( - obj.properties, - filter(isObject), - filter((p): p is Record => !isNil(p)), - some((prop) => hasCorrectIdProperty(ctxName, idName, prop)), - ); -} - -function checkReturnStatement( - n: Record, - ctxName: string, - idName: string, -): boolean { - if (!n.argument) return false; - const arg = n.argument as Record; - - // Pattern: new Person({ id: ctx.getActorUri(identifier) }) - if ( - arg.type === "NewExpression" && Array.isArray(arg.arguments) && - arg.arguments.length > 0 - ) { - const objArg = arg.arguments[0] as Record; - if (objArg.type === "ObjectExpression") { - return checkObjectExpression(objArg, ctxName, idName); - } - } - - // Pattern: { id: ctx.getActorUri(identifier) } - if (arg.type === "ObjectExpression") { - return checkObjectExpression(arg, ctxName, idName); - } - - return false; -} - -function checkIdMatchesGetActorUri( - node: unknown, - ctxName: string, - idName: string, -): boolean { - if (!isObject(node) || isNil(node)) return false; - const n = node as Record; - - // Find ReturnStatement - if (n.type === "ReturnStatement") { - return checkReturnStatement(n, ctxName, idName); - } - - // Recursively check if BlockStatement - if (n.type === "BlockStatement" && Array.isArray(n.body)) { - return pipe( - n.body, - some((stmt) => checkIdMatchesGetActorUri(stmt, ctxName, idName)), - ); - } - - return false; -} diff --git a/packages/lint/src/rules/actor-id-required.ts b/packages/lint/src/rules/actor-id-required.ts index 94a330400..15713612b 100644 --- a/packages/lint/src/rules/actor-id-required.ts +++ b/packages/lint/src/rules/actor-id-required.ts @@ -1,102 +1,20 @@ -import { filter, isNil, isObject, pipe, some } from "@fxts/core"; -import { trackFederationVariables } from "../lib/tracker.ts"; +import { properties } from "../lib/const.ts"; +import { actorPropertyRequired } from "../lib/messages.ts"; +import { createRequiredRule } from "../lib/required-rule-factory.ts"; export const ACTOR_ID_REQUIRED = "actor-id-required" as const; -const actorIdRequired: Deno.lint.Rule = { - create(context) { - const tracker = trackFederationVariables(); - - return { - VariableDeclarator: tracker.VariableDeclarator, - - CallExpression(node) { - if ( - node.callee.type === "MemberExpression" && - node.callee.property.type === "Identifier" && - node.callee.property.name === "setActorDispatcher" && - node.arguments.length >= 2 - ) { - if (!tracker.isFederationObject(node.callee.object)) return; - - const dispatcherArg = node.arguments[1]; - - if ( - dispatcherArg.type === "ArrowFunctionExpression" || - dispatcherArg.type === "FunctionExpression" - ) { - const hasIdProperty = checkForIdProperty(dispatcherArg.body); - - if (!hasIdProperty) { - context.report({ - node: dispatcherArg, - message: - "Actor dispatcher must return an actor with an `id` property. Use `Context.getActorUri(identifier)` to set it.", - }); - } - } - } - }, - }; - }, -}; +/** + * Lint rule that checks if the actor's `id` property EXISTS in the returned object. + * + * This is a `*-required` rule that only validates property existence. + * For checking if the value uses the correct `ctx.getActorUri()` method, + * use `actor-id-mismatch`. + */ +const actorIdRequired = createRequiredRule({ + propertyName: properties.id.name, + dispatcherMethod: properties.id.setter, + errorMessage: actorPropertyRequired(properties.id.name), +}); export default actorIdRequired; - -function hasIdProperty(prop: unknown): boolean { - if (!isObject(prop) || isNil(prop)) return false; - const p = prop as Record; - const key = p.key as Record; - return p.type === "Property" && - key.type === "Identifier" && - key.name === "id"; -} - -function checkObjectExpression(obj: Record): boolean { - if (!Array.isArray(obj.properties)) return false; - return pipe( - obj.properties, - filter(isObject), - filter((p): p is Record => !isNil(p)), - some(hasIdProperty), - ); -} - -function checkReturnStatement(n: Record): boolean { - if (!n.argument) return false; - const arg = n.argument as Record; - - if ( - arg.type === "NewExpression" && Array.isArray(arg.arguments) && - arg.arguments.length > 0 - ) { - const objArg = arg.arguments[0] as Record; - if (objArg.type === "ObjectExpression") { - return checkObjectExpression(objArg); - } - } - - if (arg.type === "ObjectExpression") { - return checkObjectExpression(arg); - } - - return false; -} - -function checkForIdProperty(node: unknown): boolean { - if (!isObject(node) || isNil(node)) return false; - const n = node as Record; - - if (n.type === "ReturnStatement") { - return checkReturnStatement(n); - } - - if (n.type === "BlockStatement" && Array.isArray(n.body)) { - return pipe( - n.body, - some(checkForIdProperty), - ); - } - - return false; -} diff --git a/packages/lint/src/rules/actor-inbox-property-mismatch.ts b/packages/lint/src/rules/actor-inbox-property-mismatch.ts index 2ceec6e92..5ac4d480f 100644 --- a/packages/lint/src/rules/actor-inbox-property-mismatch.ts +++ b/packages/lint/src/rules/actor-inbox-property-mismatch.ts @@ -1,4 +1,12 @@ -export const ACTOR_INBOX_PROPERTY_MISMATCH = "actor-inbox-property-mismatch"; +import { properties } from "../lib/const.ts"; +import { createMismatchRule } from "../lib/mismatch-rule-factory.ts"; + +export const ACTOR_INBOX_PROPERTY_MISMATCH = + "actor-inbox-property-mismatch" as const; + +const actorInboxPropertyMismatch: Deno.lint.Rule = createMismatchRule({ + propertyPath: properties.inbox.name, + methodName: properties.inbox.getter, +}); -const actorInboxPropertyMismatch: Deno.lint.Rule = {}; export default actorInboxPropertyMismatch; diff --git a/packages/lint/src/rules/actor-inbox-property-required.ts b/packages/lint/src/rules/actor-inbox-property-required.ts index 5ac7de701..25f37f960 100644 --- a/packages/lint/src/rules/actor-inbox-property-required.ts +++ b/packages/lint/src/rules/actor-inbox-property-required.ts @@ -1,4 +1,13 @@ +import { properties } from "../lib/const.ts"; +import { actorPropertyRequired } from "../lib/messages.ts"; +import { createRequiredRule } from "../lib/required-rule-factory.ts"; + export const ACTOR_INBOX_PROPERTY_REQUIRED = "actor-inbox-property-required"; -const actorInboxPropertyRequired: Deno.lint.Rule = {}; +const actorInboxPropertyRequired = createRequiredRule({ + propertyName: properties.inbox.name, + dispatcherMethod: properties.inbox.setter, + errorMessage: actorPropertyRequired(properties.inbox.name), +}); + export default actorInboxPropertyRequired; diff --git a/packages/lint/src/rules/actor-liked-property-mismatch.ts b/packages/lint/src/rules/actor-liked-property-mismatch.ts index 09c2db89e..1a95df34e 100644 --- a/packages/lint/src/rules/actor-liked-property-mismatch.ts +++ b/packages/lint/src/rules/actor-liked-property-mismatch.ts @@ -1,4 +1,12 @@ -export const ACTOR_LIKED_PROPERTY_MISMATCH = "actor-liked-property-mismatch"; +import { properties } from "../lib/const.ts"; +import { createMismatchRule } from "../lib/mismatch-rule-factory.ts"; + +export const ACTOR_LIKED_PROPERTY_MISMATCH = + "actor-liked-property-mismatch" as const; + +const actorLikedPropertyMismatch: Deno.lint.Rule = createMismatchRule({ + propertyPath: properties.liked.name, + methodName: properties.liked.getter, +}); -const actorLikedPropertyMismatch: Deno.lint.Rule = {}; export default actorLikedPropertyMismatch; diff --git a/packages/lint/src/rules/actor-liked-property-required.ts b/packages/lint/src/rules/actor-liked-property-required.ts index f17e3ad1f..e020af631 100644 --- a/packages/lint/src/rules/actor-liked-property-required.ts +++ b/packages/lint/src/rules/actor-liked-property-required.ts @@ -1,4 +1,13 @@ +import { properties } from "../lib/const.ts"; +import { actorPropertyRequired } from "../lib/messages.ts"; +import { createRequiredRule } from "../lib/required-rule-factory.ts"; + export const ACTOR_LIKED_PROPERTY_REQUIRED = "actor-liked-property-required"; -const actorLikedPropertyRequired: Deno.lint.Rule = {}; +const actorLikedPropertyRequired = createRequiredRule({ + propertyName: properties.liked.name, + dispatcherMethod: properties.liked.setter, + errorMessage: actorPropertyRequired(properties.liked.name), +}); + export default actorLikedPropertyRequired; diff --git a/packages/lint/src/rules/actor-outbox-property-mismatch.ts b/packages/lint/src/rules/actor-outbox-property-mismatch.ts index 3ac7feb1b..e00df1de7 100644 --- a/packages/lint/src/rules/actor-outbox-property-mismatch.ts +++ b/packages/lint/src/rules/actor-outbox-property-mismatch.ts @@ -1,4 +1,12 @@ -export const ACTOR_OUTBOX_PROPERTY_MISMATCH = "actor-outbox-property-mismatch"; +import { properties } from "../lib/const.ts"; +import { createMismatchRule } from "../lib/mismatch-rule-factory.ts"; + +export const ACTOR_OUTBOX_PROPERTY_MISMATCH = + "actor-outbox-property-mismatch" as const; + +const actorOutboxPropertyMismatch: Deno.lint.Rule = createMismatchRule({ + propertyPath: properties.outbox.name, + methodName: properties.outbox.getter, +}); -const actorOutboxPropertyMismatch: Deno.lint.Rule = {}; export default actorOutboxPropertyMismatch; diff --git a/packages/lint/src/rules/actor-outbox-property-required.ts b/packages/lint/src/rules/actor-outbox-property-required.ts index 4fefc7b92..a5822ae96 100644 --- a/packages/lint/src/rules/actor-outbox-property-required.ts +++ b/packages/lint/src/rules/actor-outbox-property-required.ts @@ -1,4 +1,13 @@ +import { properties } from "../lib/const.ts"; +import { actorPropertyRequired } from "../lib/messages.ts"; +import { createRequiredRule } from "../lib/required-rule-factory.ts"; + export const ACTOR_OUTBOX_PROPERTY_REQUIRED = "actor-outbox-property-required"; -const actorOutboxPropertyRequired: Deno.lint.Rule = {}; +const actorOutboxPropertyRequired = createRequiredRule({ + propertyName: properties.outbox.name, + dispatcherMethod: properties.outbox.setter, + errorMessage: actorPropertyRequired(properties.outbox.name), +}); + export default actorOutboxPropertyRequired; diff --git a/packages/lint/src/rules/actor-public-key-required.ts b/packages/lint/src/rules/actor-public-key-required.ts index 75f4640db..45f4ed0b6 100644 --- a/packages/lint/src/rules/actor-public-key-required.ts +++ b/packages/lint/src/rules/actor-public-key-required.ts @@ -1,4 +1,12 @@ +import { actorKeyPropertyRequired } from "../lib/messages.ts"; +import { createRequiredRule } from "../lib/required-rule-factory.ts"; + export const ACTOR_PUBLIC_KEY_REQUIRED = "actor-public-key-required"; -const actorPublicKeyRequired: Deno.lint.Rule = {}; +const actorPublicKeyRequired: Deno.lint.Rule = createRequiredRule({ + propertyName: "publicKey", + dispatcherMethod: "setKeyPairsDispatcher", + errorMessage: actorKeyPropertyRequired("publicKey"), +}); + export default actorPublicKeyRequired; diff --git a/packages/lint/src/rules/actor-shared-inbox-property-mismatch.ts b/packages/lint/src/rules/actor-shared-inbox-property-mismatch.ts index de89b4e61..52c419de5 100644 --- a/packages/lint/src/rules/actor-shared-inbox-property-mismatch.ts +++ b/packages/lint/src/rules/actor-shared-inbox-property-mismatch.ts @@ -1,5 +1,13 @@ +import { properties } from "../lib/const.ts"; +import { createMismatchRule } from "../lib/mismatch-rule-factory.ts"; + export const ACTOR_SHARED_INBOX_PROPERTY_MISMATCH = - "actor-shared-inbox-property-mismatch"; + "actor-shared-inbox-property-mismatch" as const; + +const actorSharedInboxPropertyMismatch: Deno.lint.Rule = createMismatchRule({ + propertyPath: properties.sharedInbox.name, + methodName: properties.sharedInbox.getter, + requiresIdentifier: false, +}); -const actorSharedInboxPropertyMismatch: Deno.lint.Rule = {}; export default actorSharedInboxPropertyMismatch; diff --git a/packages/lint/src/rules/actor-shared-inbox-property-required.ts b/packages/lint/src/rules/actor-shared-inbox-property-required.ts index 6c5b1e196..4956c58e1 100644 --- a/packages/lint/src/rules/actor-shared-inbox-property-required.ts +++ b/packages/lint/src/rules/actor-shared-inbox-property-required.ts @@ -1,5 +1,14 @@ +import { properties } from "../lib/const.ts"; +import { actorPropertyRequired } from "../lib/messages.ts"; +import { createRequiredRule } from "../lib/required-rule-factory.ts"; + export const ACTOR_SHARED_INBOX_PROPERTY_REQUIRED = "actor-shared-inbox-property-required"; -const actorSharedInboxPropertyRequired: Deno.lint.Rule = {}; +const actorSharedInboxPropertyRequired = createRequiredRule({ + propertyName: properties.sharedInbox.name, + dispatcherMethod: properties.sharedInbox.setter, + errorMessage: actorPropertyRequired(properties.sharedInbox.name), +}); + export default actorSharedInboxPropertyRequired; diff --git a/packages/lint/src/rules/collection-filtering-not-implemented.ts b/packages/lint/src/rules/collection-filtering-not-implemented.ts index 48f6f2bce..6e53bc903 100644 --- a/packages/lint/src/rules/collection-filtering-not-implemented.ts +++ b/packages/lint/src/rules/collection-filtering-not-implemented.ts @@ -1,5 +1,114 @@ +import { + COLLECTION_FILTERING_NOT_IMPLEMENTED_ERROR as message, +} from "../lib/messages.ts"; +import { + allOf, + hasIdentifierProperty, + hasMemberExpressionCallee, + hasMethodName, + hasMinArguments, + isFunction, +} from "../lib/pred.ts"; +import { trackFederationVariables } from "../lib/tracker.ts"; +import type { + CallMemberExpressionWithIdentified, + FunctionNode, +} from "../lib/types.ts"; + export const COLLECTION_FILTERING_NOT_IMPLEMENTED = "collection-filtering-not-implemented"; -const collectionFilteringNotImplemented: Deno.lint.Rule = {}; -export default collectionFilteringNotImplemented; +/** + * Collection dispatcher methods that support filtering. + * These are the setXxxDispatcher methods for collections. + */ +const COLLECTION_DISPATCHER_METHODS = [ + "setFollowersDispatcher", + "setFollowingDispatcher", + "setOutboxDispatcher", + "setLikedDispatcher", + "setFeaturedDispatcher", + "setFeaturedTagsDispatcher", +] as const; + +/** + * Checks if a node is a collection dispatcher call. + */ +const isCollectionDispatcherCall = ( + node: Deno.lint.CallExpression, +): node is Deno.lint.CallExpression & CallMemberExpressionWithIdentified => + allOf( + hasMemberExpressionCallee, + hasIdentifierProperty, + hasMinArguments(2), + (n: Deno.lint.CallExpression & CallMemberExpressionWithIdentified) => + COLLECTION_DISPATCHER_METHODS.some((method) => hasMethodName(method)(n)), + )(node as Deno.lint.CallExpression & CallMemberExpressionWithIdentified); + +/** + * Checks if a function node has the filter parameter (4th parameter). + * CollectionDispatcher signature: (context, identifier, cursor, filter?) => ... + */ +const hasFilterParameter = (fn: FunctionNode): boolean => { + // Filter is the 4th parameter (index 3) + return fn.params.length >= 4; +}; + +/** + * Lint rule: collection-filtering-not-implemented + * + * Warns when a collection dispatcher doesn't implement filtering. + * Collection dispatchers should accept a 4th parameter (filter) to support + * server-side filtering and avoid large response payloads. + * + * @example Good: + * ```ts + * federation.setFollowersDispatcher( + * "/users/{identifier}/followers", + * async (ctx, identifier, cursor, filter) => { + * // Implementation with filter support + * return { items: [] }; + * } + * ); + * ``` + * + * @example Bad: + * ```ts + * federation.setFollowersDispatcher( + * "/users/{identifier}/followers", + * async (ctx, identifier, cursor) => { + * // No filter parameter - will cause warning + * return { items: [] }; + * } + * ); + * ``` + */ +const collectionFilteringNotImplementedRule: Deno.lint.Rule = { + create(context) { + const federationTracker = trackFederationVariables(); + + return { + VariableDeclarator: federationTracker.VariableDeclarator, + + CallExpression(node) { + // Check if it's a collection dispatcher call on a federation object + if (!isCollectionDispatcherCall(node)) return; + if (!federationTracker.isFederationObject(node.callee.object)) return; + + // Get the dispatcher callback (2nd argument) + const dispatcherArg = node.arguments[1]; + if (!isFunction(dispatcherArg)) return; + + // Check if the callback has the filter parameter + if (!hasFilterParameter(dispatcherArg)) { + context.report({ + node: dispatcherArg, + message, + }); + } + }, + }; + }, +}; + +export default collectionFilteringNotImplementedRule; diff --git a/packages/lint/src/rules/ed25519-key-required.ts b/packages/lint/src/rules/ed25519-key-required.ts deleted file mode 100644 index e2c60fe02..000000000 --- a/packages/lint/src/rules/ed25519-key-required.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const ED25519_KEY_REQUIRED = "ed25519-key-required"; - -const ed25519KeyRequired: Deno.lint.Rule = {}; -export default ed25519KeyRequired; diff --git a/packages/lint/src/rules/rsa-key-required.ts b/packages/lint/src/rules/rsa-key-required.ts deleted file mode 100644 index 99a6f6d33..000000000 --- a/packages/lint/src/rules/rsa-key-required.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const RSA_KEY_REQUIRED = "rsa-key-required"; - -const rsaKeyRequired: Deno.lint.Rule = {}; -export default rsaKeyRequired; diff --git a/packages/lint/src/tests/actor-assertion-method-required.test.ts b/packages/lint/src/tests/actor-assertion-method-required.test.ts new file mode 100644 index 000000000..b6abc6318 --- /dev/null +++ b/packages/lint/src/tests/actor-assertion-method-required.test.ts @@ -0,0 +1,233 @@ +import { test } from "node:test"; +import { actorKeyPropertyRequired } from "../lib/messages.ts"; +import { testDenoLint } from "../lib/test.ts"; +import { + ACTOR_ASSERTION_METHOD_REQUIRED as ruleName, + default as rule, +} from "../rules/actor-assertion-method-required.ts"; + +test(`${ruleName}: ✅ Good - \`setActorDispatcher\` called on non-Federation object`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return new Person({ + name: "John Doe", + }); + }); + `, + rule, + ruleName, + federationSetup: ` + const federation = { + setActorDispatcher: () => {} + }; + `, + }); +}); + +test(`${ruleName}: ✅ Good - key pairs dispatcher NOT configured, property missing (no error)`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + name: "John Doe", + }); + }); + `, + rule, + ruleName, + }); +}); + +test(`${ruleName}: ✅ Good - key pairs dispatcher configured BEFORE setActorDispatcher, property present`, () => { + testDenoLint({ + code: ` + federation.setKeyPairsDispatcher(async (ctx, identifier) => { + return []; + }); + + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + assertionMethod: ctx.getActorKeyPairs(identifier), + name: "John Doe", + }); + }); + `, + rule, + ruleName, + }); +}); + +test(`${ruleName}: ✅ Good - key pairs dispatcher configured BEFORE setActorDispatcher (chained), property present`, () => { + testDenoLint({ + code: ` + federation + .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + assertionMethod: ctx.getActorKeyPairs(identifier), + name: "John Doe", + }); + }) + .setKeyPairsDispatcher(async (ctx, identifier) => []); + `, + rule, + ruleName, + }); +}); + +test(`${ruleName}: ✅ Good - key pairs dispatcher configured AFTER setActorDispatcher, property present`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + assertionMethod: ctx.getActorKeyPairs(identifier), + name: "John Doe", + }); + }); + + federation.setKeyPairsDispatcher(async (ctx, identifier) => { + return []; + }); + `, + rule, + ruleName, + }); +}); + +test(`${ruleName}: ✅ Good - key pairs dispatcher configured AFTER setActorDispatcher (chained), property present`, () => { + testDenoLint({ + code: ` + federation + .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + assertionMethod: ctx.getActorKeyPairs(identifier), + name: "John Doe", + }); + }) + .setKeyPairsDispatcher(async (ctx, identifier) => []); + `, + rule, + ruleName, + }); +}); + +test(`${ruleName}: ✅ Good - object literal with \`assertionMethod\``, () => { + testDenoLint({ + code: ` + federation.setKeyPairsDispatcher(async (ctx, identifier) => { + return []; + }); + + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return { + id: ctx.getActorUri(identifier), + assertionMethod: ctx.getActorKeyPairs(identifier), + name: "John Doe", + }; + }); + `, + rule, + ruleName, + }); +}); + +test(`${ruleName}: ❌ Bad - key pairs dispatcher configured BEFORE, property missing`, () => { + testDenoLint({ + code: ` + federation.setKeyPairsDispatcher(async (ctx, identifier) => { + return []; + }); + + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + name: "John Doe", + }); + }); + `, + rule, + ruleName, + expectedError: actorKeyPropertyRequired("assertionMethod"), + }); +}); + +test(`${ruleName}: ❌ Bad - key pairs dispatcher configured BEFORE (chained), property missing`, () => { + testDenoLint({ + code: ` + federation + .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + name: "John Doe", + }); + }) + .setKeyPairsDispatcher(async (ctx, identifier) => []); + `, + rule, + ruleName, + expectedError: actorKeyPropertyRequired("assertionMethod"), + }); +}); + +test(`${ruleName}: ❌ Bad - key pairs dispatcher configured AFTER, property missing`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + name: "John Doe", + }); + }); + + federation.setKeyPairsDispatcher(async (ctx, identifier) => { + return []; + }); + `, + rule, + ruleName, + expectedError: actorKeyPropertyRequired("assertionMethod"), + }); +}); + +test(`${ruleName}: ❌ Bad - key pairs dispatcher configured AFTER (chained), property missing`, () => { + testDenoLint({ + code: ` + federation + .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + name: "John Doe", + }); + }) + .setKeyPairsDispatcher(async (ctx, identifier) => []); + `, + rule, + ruleName, + expectedError: actorKeyPropertyRequired("assertionMethod"), + }); +}); + +test(`${ruleName}: ❌ Bad - object literal without property`, () => { + testDenoLint({ + code: ` + federation.setKeyPairsDispatcher(async (ctx, identifier) => { + return []; + }); + + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return { + id: ctx.getActorUri(identifier), + name: "John Doe", + }; + }); + `, + rule, + ruleName, + expectedError: actorKeyPropertyRequired("assertionMethod"), + }); +}); diff --git a/packages/lint/src/tests/actor-featured-property-mismatch.test.ts b/packages/lint/src/tests/actor-featured-property-mismatch.test.ts new file mode 100644 index 000000000..e3f9d9159 --- /dev/null +++ b/packages/lint/src/tests/actor-featured-property-mismatch.test.ts @@ -0,0 +1,59 @@ +import { test } from "node:test"; +import { + createMismatchEdgeCaseTests, + createMismatchRuleTests, +} from "../lib/test-templates.ts"; +import { + ACTOR_FEATURED_PROPERTY_MISMATCH as ruleName, + default as rule, +} from "../rules/actor-featured-property-mismatch.ts"; + +const config = { rule, ruleName }; + +// Standard mismatch rule tests +const tests = createMismatchRuleTests("featured", config); +test( + `${ruleName}: ✅ Good - non-Federation object`, + tests["non-federation object"], +); +test( + `${ruleName}: ✅ Good - correct getter used`, + tests["correct getter used"], +); +test( + `${ruleName}: ✅ Good - property not present`, + tests["property not present"], +); +test( + `${ruleName}: ✅ Good - object literal with correct getter`, + tests["object literal with correct getter"], +); +test(`${ruleName}: ❌ Bad - wrong getter used`, tests["wrong getter used"]); +test( + `${ruleName}: ❌ Bad - wrong method in object literal`, + tests["wrong method in object literal"], +); +test(`${ruleName}: ❌ Bad - wrong identifier`, tests["wrong identifier"]); + +// Edge case tests +const edgeCases = createMismatchEdgeCaseTests("featured", config); +test( + `${ruleName}: ✅ Edge - ternary with correct getter in both branches`, + edgeCases["ternary with correct getter in both branches"], +); +test( + `${ruleName}: ❌ Edge - ternary with wrong getter in consequent`, + edgeCases["ternary with wrong getter in consequent"], +); +test( + `${ruleName}: ❌ Edge - ternary with wrong getter in alternate`, + edgeCases["ternary with wrong getter in alternate"], +); +test( + `${ruleName}: ❌ Edge - ternary with wrong getter in both branches`, + edgeCases["ternary with wrong getter in both branches"], +); +test( + `${ruleName}: ✅ Edge - nested ternary with correct getter`, + edgeCases["nested ternary with correct getter"], +); diff --git a/packages/lint/src/tests/actor-featured-property-required.test.ts b/packages/lint/src/tests/actor-featured-property-required.test.ts new file mode 100644 index 000000000..f4d233d3a --- /dev/null +++ b/packages/lint/src/tests/actor-featured-property-required.test.ts @@ -0,0 +1,89 @@ +import { test } from "node:test"; +import { + createRequiredDispatcherRuleTests, + createRequiredEdgeCaseTests, +} from "../lib/test-templates.ts"; +import { + ACTOR_FEATURED_PROPERTY_REQUIRED as ruleName, + default as rule, +} from "../rules/actor-featured-property-required.ts"; + +const config = { rule, ruleName }; + +// Standard required dispatcher rule tests +const tests = createRequiredDispatcherRuleTests("featured", config); +test( + `${ruleName}: ✅ Good - non-Federation object`, + tests["non-federation object"], +); +test( + `${ruleName}: ✅ Good - dispatcher NOT configured`, + tests["dispatcher not configured"], +); +test( + `${ruleName}: ✅ Good - dispatcher BEFORE (chained) with property`, + tests["dispatcher before chained with property"], +); +test( + `${ruleName}: ✅ Good - dispatcher BEFORE (separate) with property`, + tests["dispatcher before separate with property"], +); +test( + `${ruleName}: ✅ Good - dispatcher AFTER (chained) with property`, + tests["dispatcher after chained with property"], +); +test( + `${ruleName}: ✅ Good - dispatcher AFTER (separate) with property`, + tests["dispatcher after separate with property"], +); +test( + `${ruleName}: ✅ Good - object literal with property`, + tests["object literal with property"], +); +test( + `${ruleName}: ❌ Bad - dispatcher configured, property missing`, + tests["dispatcher configured property missing"], +); +test( + `${ruleName}: ❌ Bad - dispatcher BEFORE (separate), property missing`, + tests["dispatcher before separate property missing"], +); +test( + `${ruleName}: ❌ Bad - dispatcher AFTER (separate), property missing`, + tests["dispatcher after separate property missing"], +); +test( + `${ruleName}: ❌ Bad - object literal without property`, + tests["object literal without property"], +); +test( + `${ruleName}: ❌ Bad - variable assignment without property`, + tests["variable assignment without property"], +); + +// Edge case tests +const edgeCases = createRequiredEdgeCaseTests( + "featured", + config, + "setFeaturedDispatcher", +); +test( + `${ruleName}: ✅ Edge - ternary with property in both branches`, + edgeCases["ternary with property in both branches"], +); +test( + `${ruleName}: ❌ Edge - ternary missing property in consequent`, + edgeCases["ternary missing property in consequent"], +); +test( + `${ruleName}: ❌ Edge - ternary missing property in alternate`, + edgeCases["ternary missing property in alternate"], +); +test( + `${ruleName}: ❌ Edge - ternary missing property in both branches`, + edgeCases["ternary missing property in both branches"], +); +test( + `${ruleName}: ✅ Edge - nested ternary with property`, + edgeCases["nested ternary with property"], +); diff --git a/packages/lint/src/tests/actor-featured-tags-property-mismatch.test.ts b/packages/lint/src/tests/actor-featured-tags-property-mismatch.test.ts new file mode 100644 index 000000000..f3e5e2eaa --- /dev/null +++ b/packages/lint/src/tests/actor-featured-tags-property-mismatch.test.ts @@ -0,0 +1,59 @@ +import { test } from "node:test"; +import { + createMismatchEdgeCaseTests, + createMismatchRuleTests, +} from "../lib/test-templates.ts"; +import { + ACTOR_FEATURED_TAGS_PROPERTY_MISMATCH as ruleName, + default as rule, +} from "../rules/actor-featured-tags-property-mismatch.ts"; + +const config = { rule, ruleName }; + +// Standard mismatch rule tests +const tests = createMismatchRuleTests("featuredTags", config); +test( + `${ruleName}: ✅ Good - non-Federation object`, + tests["non-federation object"], +); +test( + `${ruleName}: ✅ Good - correct getter used`, + tests["correct getter used"], +); +test( + `${ruleName}: ✅ Good - property not present`, + tests["property not present"], +); +test( + `${ruleName}: ✅ Good - object literal with correct getter`, + tests["object literal with correct getter"], +); +test(`${ruleName}: ❌ Bad - wrong getter used`, tests["wrong getter used"]); +test( + `${ruleName}: ❌ Bad - wrong method in object literal`, + tests["wrong method in object literal"], +); +test(`${ruleName}: ❌ Bad - wrong identifier`, tests["wrong identifier"]); + +// Edge case tests +const edgeCases = createMismatchEdgeCaseTests("featuredTags", config); +test( + `${ruleName}: ✅ Edge - ternary with correct getter in both branches`, + edgeCases["ternary with correct getter in both branches"], +); +test( + `${ruleName}: ❌ Edge - ternary with wrong getter in consequent`, + edgeCases["ternary with wrong getter in consequent"], +); +test( + `${ruleName}: ❌ Edge - ternary with wrong getter in alternate`, + edgeCases["ternary with wrong getter in alternate"], +); +test( + `${ruleName}: ❌ Edge - ternary with wrong getter in both branches`, + edgeCases["ternary with wrong getter in both branches"], +); +test( + `${ruleName}: ✅ Edge - nested ternary with correct getter`, + edgeCases["nested ternary with correct getter"], +); diff --git a/packages/lint/src/tests/actor-featured-tags-property-required.test.ts b/packages/lint/src/tests/actor-featured-tags-property-required.test.ts new file mode 100644 index 000000000..016c29862 --- /dev/null +++ b/packages/lint/src/tests/actor-featured-tags-property-required.test.ts @@ -0,0 +1,89 @@ +import { test } from "node:test"; +import { + createRequiredDispatcherRuleTests, + createRequiredEdgeCaseTests, +} from "../lib/test-templates.ts"; +import { + ACTOR_FEATURED_TAGS_PROPERTY_REQUIRED as ruleName, + default as rule, +} from "../rules/actor-featured-tags-property-required.ts"; + +const config = { rule, ruleName }; + +// Standard required dispatcher rule tests +const tests = createRequiredDispatcherRuleTests("featuredTags", config); +test( + `${ruleName}: ✅ Good - non-Federation object`, + tests["non-federation object"], +); +test( + `${ruleName}: ✅ Good - dispatcher NOT configured`, + tests["dispatcher not configured"], +); +test( + `${ruleName}: ✅ Good - dispatcher BEFORE (chained) with property`, + tests["dispatcher before chained with property"], +); +test( + `${ruleName}: ✅ Good - dispatcher BEFORE (separate) with property`, + tests["dispatcher before separate with property"], +); +test( + `${ruleName}: ✅ Good - dispatcher AFTER (chained) with property`, + tests["dispatcher after chained with property"], +); +test( + `${ruleName}: ✅ Good - dispatcher AFTER (separate) with property`, + tests["dispatcher after separate with property"], +); +test( + `${ruleName}: ✅ Good - object literal with property`, + tests["object literal with property"], +); +test( + `${ruleName}: ❌ Bad - dispatcher configured, property missing`, + tests["dispatcher configured property missing"], +); +test( + `${ruleName}: ❌ Bad - dispatcher BEFORE (separate), property missing`, + tests["dispatcher before separate property missing"], +); +test( + `${ruleName}: ❌ Bad - dispatcher AFTER (separate), property missing`, + tests["dispatcher after separate property missing"], +); +test( + `${ruleName}: ❌ Bad - object literal without property`, + tests["object literal without property"], +); +test( + `${ruleName}: ❌ Bad - variable assignment without property`, + tests["variable assignment without property"], +); + +// Edge case tests +const edgeCases = createRequiredEdgeCaseTests( + "featuredTags", + config, + "setFeaturedTagsDispatcher", +); +test( + `${ruleName}: ✅ Edge - ternary with property in both branches`, + edgeCases["ternary with property in both branches"], +); +test( + `${ruleName}: ❌ Edge - ternary missing property in consequent`, + edgeCases["ternary missing property in consequent"], +); +test( + `${ruleName}: ❌ Edge - ternary missing property in alternate`, + edgeCases["ternary missing property in alternate"], +); +test( + `${ruleName}: ❌ Edge - ternary missing property in both branches`, + edgeCases["ternary missing property in both branches"], +); +test( + `${ruleName}: ✅ Edge - nested ternary with property`, + edgeCases["nested ternary with property"], +); diff --git a/packages/lint/src/tests/actor-followers-property-mismatch.test.ts b/packages/lint/src/tests/actor-followers-property-mismatch.test.ts new file mode 100644 index 000000000..17fa972ad --- /dev/null +++ b/packages/lint/src/tests/actor-followers-property-mismatch.test.ts @@ -0,0 +1,59 @@ +import { test } from "node:test"; +import { + createMismatchEdgeCaseTests, + createMismatchRuleTests, +} from "../lib/test-templates.ts"; +import { + ACTOR_FOLLOWERS_PROPERTY_MISMATCH as ruleName, + default as rule, +} from "../rules/actor-followers-property-mismatch.ts"; + +const config = { rule, ruleName }; + +// Standard mismatch rule tests +const tests = createMismatchRuleTests("followers", config); +test( + `${ruleName}: ✅ Good - non-Federation object`, + tests["non-federation object"], +); +test( + `${ruleName}: ✅ Good - correct getter used`, + tests["correct getter used"], +); +test( + `${ruleName}: ✅ Good - property not present`, + tests["property not present"], +); +test( + `${ruleName}: ✅ Good - object literal with correct getter`, + tests["object literal with correct getter"], +); +test(`${ruleName}: ❌ Bad - wrong getter used`, tests["wrong getter used"]); +test( + `${ruleName}: ❌ Bad - wrong method in object literal`, + tests["wrong method in object literal"], +); +test(`${ruleName}: ❌ Bad - wrong identifier`, tests["wrong identifier"]); + +// Edge case tests +const edgeCases = createMismatchEdgeCaseTests("followers", config); +test( + `${ruleName}: ✅ Edge - ternary with correct getter in both branches`, + edgeCases["ternary with correct getter in both branches"], +); +test( + `${ruleName}: ❌ Edge - ternary with wrong getter in consequent`, + edgeCases["ternary with wrong getter in consequent"], +); +test( + `${ruleName}: ❌ Edge - ternary with wrong getter in alternate`, + edgeCases["ternary with wrong getter in alternate"], +); +test( + `${ruleName}: ❌ Edge - ternary with wrong getter in both branches`, + edgeCases["ternary with wrong getter in both branches"], +); +test( + `${ruleName}: ✅ Edge - nested ternary with correct getter`, + edgeCases["nested ternary with correct getter"], +); diff --git a/packages/lint/src/tests/actor-followers-property-required.test.ts b/packages/lint/src/tests/actor-followers-property-required.test.ts new file mode 100644 index 000000000..b57e6886f --- /dev/null +++ b/packages/lint/src/tests/actor-followers-property-required.test.ts @@ -0,0 +1,89 @@ +import { test } from "node:test"; +import { + createRequiredDispatcherRuleTests, + createRequiredEdgeCaseTests, +} from "../lib/test-templates.ts"; +import { + ACTOR_FOLLOWERS_PROPERTY_REQUIRED as ruleName, + default as rule, +} from "../rules/actor-followers-property-required.ts"; + +const config = { rule, ruleName }; + +// Standard required dispatcher rule tests +const tests = createRequiredDispatcherRuleTests("followers", config); +test( + `${ruleName}: ✅ Good - non-Federation object`, + tests["non-federation object"], +); +test( + `${ruleName}: ✅ Good - dispatcher NOT configured`, + tests["dispatcher not configured"], +); +test( + `${ruleName}: ✅ Good - dispatcher BEFORE (chained) with property`, + tests["dispatcher before chained with property"], +); +test( + `${ruleName}: ✅ Good - dispatcher BEFORE (separate) with property`, + tests["dispatcher before separate with property"], +); +test( + `${ruleName}: ✅ Good - dispatcher AFTER (chained) with property`, + tests["dispatcher after chained with property"], +); +test( + `${ruleName}: ✅ Good - dispatcher AFTER (separate) with property`, + tests["dispatcher after separate with property"], +); +test( + `${ruleName}: ✅ Good - object literal with property`, + tests["object literal with property"], +); +test( + `${ruleName}: ❌ Bad - dispatcher configured, property missing`, + tests["dispatcher configured property missing"], +); +test( + `${ruleName}: ❌ Bad - dispatcher BEFORE (separate), property missing`, + tests["dispatcher before separate property missing"], +); +test( + `${ruleName}: ❌ Bad - dispatcher AFTER (separate), property missing`, + tests["dispatcher after separate property missing"], +); +test( + `${ruleName}: ❌ Bad - object literal without property`, + tests["object literal without property"], +); +test( + `${ruleName}: ❌ Bad - variable assignment without property`, + tests["variable assignment without property"], +); + +// Edge case tests +const edgeCases = createRequiredEdgeCaseTests( + "followers", + config, + "setFollowersDispatcher", +); +test( + `${ruleName}: ✅ Edge - ternary with property in both branches`, + edgeCases["ternary with property in both branches"], +); +test( + `${ruleName}: ❌ Edge - ternary missing property in consequent`, + edgeCases["ternary missing property in consequent"], +); +test( + `${ruleName}: ❌ Edge - ternary missing property in alternate`, + edgeCases["ternary missing property in alternate"], +); +test( + `${ruleName}: ❌ Edge - ternary missing property in both branches`, + edgeCases["ternary missing property in both branches"], +); +test( + `${ruleName}: ✅ Edge - nested ternary with property`, + edgeCases["nested ternary with property"], +); diff --git a/packages/lint/src/tests/actor-following-property-mismatch.test.ts b/packages/lint/src/tests/actor-following-property-mismatch.test.ts new file mode 100644 index 000000000..d43aa89dd --- /dev/null +++ b/packages/lint/src/tests/actor-following-property-mismatch.test.ts @@ -0,0 +1,59 @@ +import { test } from "node:test"; +import { + createMismatchEdgeCaseTests, + createMismatchRuleTests, +} from "../lib/test-templates.ts"; +import { + ACTOR_FOLLOWING_PROPERTY_MISMATCH as ruleName, + default as rule, +} from "../rules/actor-following-property-mismatch.ts"; + +const config = { rule, ruleName }; + +// Standard mismatch rule tests +const tests = createMismatchRuleTests("following", config); +test( + `${ruleName}: ✅ Good - non-Federation object`, + tests["non-federation object"], +); +test( + `${ruleName}: ✅ Good - correct getter used`, + tests["correct getter used"], +); +test( + `${ruleName}: ✅ Good - property not present`, + tests["property not present"], +); +test( + `${ruleName}: ✅ Good - object literal with correct getter`, + tests["object literal with correct getter"], +); +test(`${ruleName}: ❌ Bad - wrong getter used`, tests["wrong getter used"]); +test( + `${ruleName}: ❌ Bad - wrong method in object literal`, + tests["wrong method in object literal"], +); +test(`${ruleName}: ❌ Bad - wrong identifier`, tests["wrong identifier"]); + +// Edge case tests +const edgeCases = createMismatchEdgeCaseTests("following", config); +test( + `${ruleName}: ✅ Edge - ternary with correct getter in both branches`, + edgeCases["ternary with correct getter in both branches"], +); +test( + `${ruleName}: ❌ Edge - ternary with wrong getter in consequent`, + edgeCases["ternary with wrong getter in consequent"], +); +test( + `${ruleName}: ❌ Edge - ternary with wrong getter in alternate`, + edgeCases["ternary with wrong getter in alternate"], +); +test( + `${ruleName}: ❌ Edge - ternary with wrong getter in both branches`, + edgeCases["ternary with wrong getter in both branches"], +); +test( + `${ruleName}: ✅ Edge - nested ternary with correct getter`, + edgeCases["nested ternary with correct getter"], +); diff --git a/packages/lint/src/tests/actor-following-property-required.test.ts b/packages/lint/src/tests/actor-following-property-required.test.ts new file mode 100644 index 000000000..ac3b6b196 --- /dev/null +++ b/packages/lint/src/tests/actor-following-property-required.test.ts @@ -0,0 +1,89 @@ +import { test } from "node:test"; +import { + createRequiredDispatcherRuleTests, + createRequiredEdgeCaseTests, +} from "../lib/test-templates.ts"; +import { + ACTOR_FOLLOWING_PROPERTY_REQUIRED as ruleName, + default as rule, +} from "../rules/actor-following-property-required.ts"; + +const config = { rule, ruleName }; + +// Standard required dispatcher rule tests +const tests = createRequiredDispatcherRuleTests("following", config); +test( + `${ruleName}: ✅ Good - non-Federation object`, + tests["non-federation object"], +); +test( + `${ruleName}: ✅ Good - dispatcher NOT configured`, + tests["dispatcher not configured"], +); +test( + `${ruleName}: ✅ Good - dispatcher BEFORE (chained) with property`, + tests["dispatcher before chained with property"], +); +test( + `${ruleName}: ✅ Good - dispatcher BEFORE (separate) with property`, + tests["dispatcher before separate with property"], +); +test( + `${ruleName}: ✅ Good - dispatcher AFTER (chained) with property`, + tests["dispatcher after chained with property"], +); +test( + `${ruleName}: ✅ Good - dispatcher AFTER (separate) with property`, + tests["dispatcher after separate with property"], +); +test( + `${ruleName}: ✅ Good - object literal with property`, + tests["object literal with property"], +); +test( + `${ruleName}: ❌ Bad - dispatcher configured, property missing`, + tests["dispatcher configured property missing"], +); +test( + `${ruleName}: ❌ Bad - dispatcher BEFORE (separate), property missing`, + tests["dispatcher before separate property missing"], +); +test( + `${ruleName}: ❌ Bad - dispatcher AFTER (separate), property missing`, + tests["dispatcher after separate property missing"], +); +test( + `${ruleName}: ❌ Bad - object literal without property`, + tests["object literal without property"], +); +test( + `${ruleName}: ❌ Bad - variable assignment without property`, + tests["variable assignment without property"], +); + +// Edge case tests +const edgeCases = createRequiredEdgeCaseTests( + "following", + config, + "setFollowingDispatcher", +); +test( + `${ruleName}: ✅ Edge - ternary with property in both branches`, + edgeCases["ternary with property in both branches"], +); +test( + `${ruleName}: ❌ Edge - ternary missing property in consequent`, + edgeCases["ternary missing property in consequent"], +); +test( + `${ruleName}: ❌ Edge - ternary missing property in alternate`, + edgeCases["ternary missing property in alternate"], +); +test( + `${ruleName}: ❌ Edge - ternary missing property in both branches`, + edgeCases["ternary missing property in both branches"], +); +test( + `${ruleName}: ✅ Edge - nested ternary with property`, + edgeCases["nested ternary with property"], +); diff --git a/packages/lint/src/tests/actor-id-mismatch.test.ts b/packages/lint/src/tests/actor-id-mismatch.test.ts index 446f932b5..615ed2cc6 100644 --- a/packages/lint/src/tests/actor-id-mismatch.test.ts +++ b/packages/lint/src/tests/actor-id-mismatch.test.ts @@ -1,4 +1,5 @@ import { test } from "node:test"; +import { actorPropertyMismatch } from "../lib/messages.ts"; import { testDenoLint } from "../lib/test.ts"; import { ACTOR_ID_MISMATCH as ruleName, @@ -10,6 +11,7 @@ test(`${ruleName}: ✅ Good - \`setActorDispatcher\` called on non-Federation ob code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { return new Person({ + id: "https://example.com/users/123", name: "John Doe", }); }); @@ -24,7 +26,7 @@ test(`${ruleName}: ✅ Good - \`setActorDispatcher\` called on non-Federation ob }); }); -test(`${ruleName}: ✅ Good - \`id\` from \`ctx.getActorUri(identifier)\``, () => { +test(`${ruleName}: ✅ Good - id uses ctx.getActorUri(identifier)`, () => { testDenoLint({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { @@ -39,39 +41,54 @@ test(`${ruleName}: ✅ Good - \`id\` from \`ctx.getActorUri(identifier)\``, () = }); }); -test(`${ruleName}: ❌ Bad - \`id\` from not using \`ctx.getActorUri(identifier)\``, () => { +test(`${ruleName}: ✅ Good - object literal with correct id`, () => { testDenoLint({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ - id: new URL("https://example.com/user/john"), + return { + id: ctx.getActorUri(identifier), name: "John Doe", + }; + }); + `, + rule, + ruleName, + }); +}); + +test(`${ruleName}: ✅ Good - BlockStatement with correct id`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + const name = "John Doe"; + return new Person({ + id: ctx.getActorUri(identifier), + name, }); }); `, rule, ruleName, - expectedError: - "Actor's `id` property must match `ctx.getActorUri(identifier)`", }); +}); + +test(`${ruleName}: ❌ Bad - id uses hardcoded string instead of ctx.getActorUri()`, () => { testDenoLint({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - const someOtherVariable = new URL("https://example.com/user/john"); return new Person({ - id: someOtherVariable, + id: "https://example.com/users/123", name: "John Doe", }); }); `, rule, ruleName, - expectedError: - "Actor's `id` property must match `ctx.getActorUri(identifier)`", + expectedError: actorPropertyMismatch("id", "ctx.getActorUri(identifier)"), }); }); -test(`${ruleName}: ❌ Bad - \`id\` using wrong context method`, () => { +test(`${ruleName}: ❌ Bad - id uses wrong method (getInboxUri instead of getActorUri)`, () => { testDenoLint({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { @@ -83,7 +100,124 @@ test(`${ruleName}: ❌ Bad - \`id\` using wrong context method`, () => { `, rule, ruleName, - expectedError: - "Actor's `id` property must match `ctx.getActorUri(identifier)`", + expectedError: actorPropertyMismatch("id", "ctx.getActorUri(identifier)"), + }); +}); + +test(`${ruleName}: ❌ Bad - id uses wrong identifier parameter`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri("wrong"), + name: "John Doe", + }); + }); + `, + rule, + ruleName, + expectedError: actorPropertyMismatch("id", "ctx.getActorUri(identifier)"), + }); +}); + +test(`${ruleName}: ❌ Bad - object literal with wrong id`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return { + id: "https://example.com/users/123", + name: "John Doe", + }; + }); + `, + rule, + ruleName, + expectedError: actorPropertyMismatch("id", "ctx.getActorUri(identifier)"), + }); +}); + +test(`${ruleName} Edge: ✅ multiple return statements - all correct`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + if (identifier === "admin") { + return new Person({ id: ctx.getActorUri(identifier), name: "Admin" }); + } + return new Person({ id: ctx.getActorUri(identifier), name: "User" }); + }); + `, + rule, + ruleName, + }); +}); + +test(`${ruleName} Edge: ⚠️ multiple returns - known limitation`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + if (identifier === "admin") { + return new Person({ id: "hardcoded", name: "Admin" }); + } + return new Person({ id: ctx.getActorUri(identifier), name: "User" }); + }); + `, + rule, + ruleName, + // Known limitation: Once ANY return has correct id, the rule passes. + // The first return with wrong id is not caught. + }); +}); + +test(`${ruleName} Edge: ✅ spread operator with correct id after spread`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + const base = { name: "User" }; + return new Person({ ...base, id: ctx.getActorUri(identifier) }); + }); + `, + rule, + ruleName, + }); +}); + +test(`${ruleName} Edge: ❌ spread operator with wrong id after spread`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + const base = { name: "User" }; + return new Person({ ...base, id: "hardcoded" }); + }); + `, + rule, + ruleName, + expectedError: actorPropertyMismatch("id", "ctx.getActorUri(identifier)"), + }); +}); + +test(`${ruleName} Edge: ✅ arrow function direct return with correct id`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => ({ + id: ctx.getActorUri(identifier), + name: "User", + })); + `, + rule, + ruleName, + }); +}); + +test(`${ruleName} Edge: ❌ arrow function direct return with wrong id`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => ({ + id: "hardcoded", + name: "User", + })); + `, + rule, + ruleName, + expectedError: actorPropertyMismatch("id", "ctx.getActorUri(identifier)"), }); }); diff --git a/packages/lint/src/tests/actor-id-required.test.ts b/packages/lint/src/tests/actor-id-required.test.ts index 53e64c025..0d018345d 100644 --- a/packages/lint/src/tests/actor-id-required.test.ts +++ b/packages/lint/src/tests/actor-id-required.test.ts @@ -1,4 +1,6 @@ import { test } from "node:test"; +import { properties } from "../lib/const.ts"; +import { actorPropertyRequired } from "../lib/messages.ts"; import { testDenoLint } from "../lib/test.ts"; import { ACTOR_ID_REQUIRED as ruleName, @@ -24,7 +26,22 @@ test(`${ruleName}: ✅ Good - \`setActorDispatcher\` called on non-Federation ob }); }); -test(`${ruleName}: ✅ Good - with \`id\` property`, () => { +test(`${ruleName}: ✅ Good - with \`id\` property (any value)`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return new Person({ + id: "https://example.com/users/123", + name: "John Doe", + }); + }); + `, + rule, + ruleName, + }); +}); + +test(`${ruleName}: ✅ Good - with \`id\` property using ctx.getActorUri()`, () => { testDenoLint({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { @@ -44,7 +61,7 @@ test(`${ruleName}: ✅ Good - object literal with \`id\``, () => { code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { return { - id: ctx.getActorUri(identifier), + id: "https://example.com/users/123", name: "John Doe", }; }); @@ -81,8 +98,7 @@ test(`${ruleName}: ❌ Bad - without \`id\` property`, () => { `, rule, ruleName, - expectedError: - "Actor dispatcher must return an actor with an `id` property. Use `Context.getActorUri(identifier)` to set it.", + expectedError: actorPropertyRequired(properties.id.name), }); }); @@ -95,29 +111,28 @@ test(`${ruleName}: ❌ Bad - returning empty object`, () => { `, rule, ruleName, - expectedError: - "Actor dispatcher must return an actor with an `id` property", + expectedError: actorPropertyRequired(properties.id.name), }); }); -test(`${ruleName}: ❌ Bad - object literal without \`id\``, () => { +test(`${ruleName}: ✅ Good - multiple properties including \`id\``, () => { testDenoLint({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return { + return new Person({ + id: ctx.getActorUri(identifier), name: "John Doe", - followers: ctx.getFollowersUri(identifier), - }; + inbox: ctx.getInboxUri(identifier), + outbox: ctx.getOutboxUri(identifier), + }); }); `, rule, ruleName, - expectedError: - "Actor dispatcher must return an actor with an `id` property", }); }); -test(`${ruleName}: ❌ Bad - BlockStatement without \`id\``, () => { +test(`${ruleName}: ❌ Bad - variable assignment without \`id\``, () => { testDenoLint({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { @@ -129,7 +144,21 @@ test(`${ruleName}: ❌ Bad - BlockStatement without \`id\``, () => { `, rule, ruleName, - expectedError: - "Actor dispatcher must return an actor with an `id` property", + expectedError: actorPropertyRequired(properties.id.name), + }); +}); + +test(`${ruleName}: ❌ Bad - object literal without \`id\``, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return { + name: "John Doe", + }; + }); + `, + rule, + ruleName, + expectedError: actorPropertyRequired(properties.id.name), }); }); diff --git a/packages/lint/src/tests/actor-inbox-property-mismatch.test.ts b/packages/lint/src/tests/actor-inbox-property-mismatch.test.ts new file mode 100644 index 000000000..5fc13d47d --- /dev/null +++ b/packages/lint/src/tests/actor-inbox-property-mismatch.test.ts @@ -0,0 +1,59 @@ +import { test } from "node:test"; +import { + createMismatchEdgeCaseTests, + createMismatchRuleTests, +} from "../lib/test-templates.ts"; +import { + ACTOR_INBOX_PROPERTY_MISMATCH as ruleName, + default as rule, +} from "../rules/actor-inbox-property-mismatch.ts"; + +const config = { rule, ruleName }; + +// Standard mismatch rule tests +const tests = createMismatchRuleTests("inbox", config); +test( + `${ruleName}: ✅ Good - non-Federation object`, + tests["non-federation object"], +); +test( + `${ruleName}: ✅ Good - correct getter used`, + tests["correct getter used"], +); +test( + `${ruleName}: ✅ Good - property not present`, + tests["property not present"], +); +test( + `${ruleName}: ✅ Good - object literal with correct getter`, + tests["object literal with correct getter"], +); +test(`${ruleName}: ❌ Bad - wrong getter used`, tests["wrong getter used"]); +test( + `${ruleName}: ❌ Bad - wrong method in object literal`, + tests["wrong method in object literal"], +); +test(`${ruleName}: ❌ Bad - wrong identifier`, tests["wrong identifier"]); + +// Edge case tests +const edgeCases = createMismatchEdgeCaseTests("inbox", config); +test( + `${ruleName}: ✅ Edge - ternary with correct getter in both branches`, + edgeCases["ternary with correct getter in both branches"], +); +test( + `${ruleName}: ❌ Edge - ternary with wrong getter in consequent`, + edgeCases["ternary with wrong getter in consequent"], +); +test( + `${ruleName}: ❌ Edge - ternary with wrong getter in alternate`, + edgeCases["ternary with wrong getter in alternate"], +); +test( + `${ruleName}: ❌ Edge - ternary with wrong getter in both branches`, + edgeCases["ternary with wrong getter in both branches"], +); +test( + `${ruleName}: ✅ Edge - nested ternary with correct getter`, + edgeCases["nested ternary with correct getter"], +); diff --git a/packages/lint/src/tests/actor-inbox-property-required.test.ts b/packages/lint/src/tests/actor-inbox-property-required.test.ts new file mode 100644 index 000000000..e36b53a7c --- /dev/null +++ b/packages/lint/src/tests/actor-inbox-property-required.test.ts @@ -0,0 +1,89 @@ +import { test } from "node:test"; +import { + createRequiredEdgeCaseTests, + createRequiredListenerRuleTests, +} from "../lib/test-templates.ts"; +import { + ACTOR_INBOX_PROPERTY_REQUIRED as ruleName, + default as rule, +} from "../rules/actor-inbox-property-required.ts"; + +const config = { rule, ruleName }; + +// Standard required listener rule tests +const tests = createRequiredListenerRuleTests("inbox", config); +test( + `${ruleName}: ✅ Good - non-Federation object`, + tests["non-federation object"], +); +test( + `${ruleName}: ✅ Good - listeners NOT configured`, + tests["listeners not configured"], +); +test( + `${ruleName}: ✅ Good - listeners BEFORE (chained) with property`, + tests["listeners before chained with property"], +); +test( + `${ruleName}: ✅ Good - listeners BEFORE (separate) with property`, + tests["listeners before separate with property"], +); +test( + `${ruleName}: ✅ Good - listeners AFTER (chained) with property`, + tests["listeners after chained with property"], +); +test( + `${ruleName}: ✅ Good - listeners AFTER (separate) with property`, + tests["listeners after separate with property"], +); +test( + `${ruleName}: ✅ Good - object literal with property`, + tests["object literal with property"], +); +test( + `${ruleName}: ❌ Bad - listeners configured, property missing`, + tests["listeners configured property missing"], +); +test( + `${ruleName}: ❌ Bad - listeners BEFORE (separate), property missing`, + tests["listeners before separate property missing"], +); +test( + `${ruleName}: ❌ Bad - listeners AFTER (separate), property missing`, + tests["listeners after separate property missing"], +); +test( + `${ruleName}: ❌ Bad - object literal without property`, + tests["object literal without property"], +); +test( + `${ruleName}: ❌ Bad - variable assignment without property`, + tests["variable assignment without property"], +); + +// Edge case tests +const edgeCases = createRequiredEdgeCaseTests( + "inbox", + config, + "setInboxListeners", +); +test( + `${ruleName}: ✅ Edge - ternary with property in both branches`, + edgeCases["ternary with property in both branches"], +); +test( + `${ruleName}: ❌ Edge - ternary missing property in consequent`, + edgeCases["ternary missing property in consequent"], +); +test( + `${ruleName}: ❌ Edge - ternary missing property in alternate`, + edgeCases["ternary missing property in alternate"], +); +test( + `${ruleName}: ❌ Edge - ternary missing property in both branches`, + edgeCases["ternary missing property in both branches"], +); +test( + `${ruleName}: ✅ Edge - nested ternary with property`, + edgeCases["nested ternary with property"], +); diff --git a/packages/lint/src/tests/actor-liked-property-mismatch.test.ts b/packages/lint/src/tests/actor-liked-property-mismatch.test.ts new file mode 100644 index 000000000..cdead140f --- /dev/null +++ b/packages/lint/src/tests/actor-liked-property-mismatch.test.ts @@ -0,0 +1,59 @@ +import { test } from "node:test"; +import { + createMismatchEdgeCaseTests, + createMismatchRuleTests, +} from "../lib/test-templates.ts"; +import { + ACTOR_LIKED_PROPERTY_MISMATCH as ruleName, + default as rule, +} from "../rules/actor-liked-property-mismatch.ts"; + +const config = { rule, ruleName }; + +// Standard mismatch rule tests +const tests = createMismatchRuleTests("liked", config); +test( + `${ruleName}: ✅ Good - non-Federation object`, + tests["non-federation object"], +); +test( + `${ruleName}: ✅ Good - correct getter used`, + tests["correct getter used"], +); +test( + `${ruleName}: ✅ Good - property not present`, + tests["property not present"], +); +test( + `${ruleName}: ✅ Good - object literal with correct getter`, + tests["object literal with correct getter"], +); +test(`${ruleName}: ❌ Bad - wrong getter used`, tests["wrong getter used"]); +test( + `${ruleName}: ❌ Bad - wrong method in object literal`, + tests["wrong method in object literal"], +); +test(`${ruleName}: ❌ Bad - wrong identifier`, tests["wrong identifier"]); + +// Edge case tests +const edgeCases = createMismatchEdgeCaseTests("liked", config); +test( + `${ruleName}: ✅ Edge - ternary with correct getter in both branches`, + edgeCases["ternary with correct getter in both branches"], +); +test( + `${ruleName}: ❌ Edge - ternary with wrong getter in consequent`, + edgeCases["ternary with wrong getter in consequent"], +); +test( + `${ruleName}: ❌ Edge - ternary with wrong getter in alternate`, + edgeCases["ternary with wrong getter in alternate"], +); +test( + `${ruleName}: ❌ Edge - ternary with wrong getter in both branches`, + edgeCases["ternary with wrong getter in both branches"], +); +test( + `${ruleName}: ✅ Edge - nested ternary with correct getter`, + edgeCases["nested ternary with correct getter"], +); diff --git a/packages/lint/src/tests/actor-liked-property-required.test.ts b/packages/lint/src/tests/actor-liked-property-required.test.ts new file mode 100644 index 000000000..94c18f7d8 --- /dev/null +++ b/packages/lint/src/tests/actor-liked-property-required.test.ts @@ -0,0 +1,89 @@ +import { test } from "node:test"; +import { + createRequiredDispatcherRuleTests, + createRequiredEdgeCaseTests, +} from "../lib/test-templates.ts"; +import { + ACTOR_LIKED_PROPERTY_REQUIRED as ruleName, + default as rule, +} from "../rules/actor-liked-property-required.ts"; + +const config = { rule, ruleName }; + +// Standard required dispatcher rule tests +const tests = createRequiredDispatcherRuleTests("liked", config); +test( + `${ruleName}: ✅ Good - non-Federation object`, + tests["non-federation object"], +); +test( + `${ruleName}: ✅ Good - dispatcher NOT configured`, + tests["dispatcher not configured"], +); +test( + `${ruleName}: ✅ Good - dispatcher BEFORE (chained) with property`, + tests["dispatcher before chained with property"], +); +test( + `${ruleName}: ✅ Good - dispatcher BEFORE (separate) with property`, + tests["dispatcher before separate with property"], +); +test( + `${ruleName}: ✅ Good - dispatcher AFTER (chained) with property`, + tests["dispatcher after chained with property"], +); +test( + `${ruleName}: ✅ Good - dispatcher AFTER (separate) with property`, + tests["dispatcher after separate with property"], +); +test( + `${ruleName}: ✅ Good - object literal with property`, + tests["object literal with property"], +); +test( + `${ruleName}: ❌ Bad - dispatcher configured, property missing`, + tests["dispatcher configured property missing"], +); +test( + `${ruleName}: ❌ Bad - dispatcher BEFORE (separate), property missing`, + tests["dispatcher before separate property missing"], +); +test( + `${ruleName}: ❌ Bad - dispatcher AFTER (separate), property missing`, + tests["dispatcher after separate property missing"], +); +test( + `${ruleName}: ❌ Bad - object literal without property`, + tests["object literal without property"], +); +test( + `${ruleName}: ❌ Bad - variable assignment without property`, + tests["variable assignment without property"], +); + +// Edge case tests +const edgeCases = createRequiredEdgeCaseTests( + "liked", + config, + "setLikedDispatcher", +); +test( + `${ruleName}: ✅ Edge - ternary with property in both branches`, + edgeCases["ternary with property in both branches"], +); +test( + `${ruleName}: ❌ Edge - ternary missing property in consequent`, + edgeCases["ternary missing property in consequent"], +); +test( + `${ruleName}: ❌ Edge - ternary missing property in alternate`, + edgeCases["ternary missing property in alternate"], +); +test( + `${ruleName}: ❌ Edge - ternary missing property in both branches`, + edgeCases["ternary missing property in both branches"], +); +test( + `${ruleName}: ✅ Edge - nested ternary with property`, + edgeCases["nested ternary with property"], +); diff --git a/packages/lint/src/tests/actor-outbox-property-mismatch.test.ts b/packages/lint/src/tests/actor-outbox-property-mismatch.test.ts new file mode 100644 index 000000000..d047e60c9 --- /dev/null +++ b/packages/lint/src/tests/actor-outbox-property-mismatch.test.ts @@ -0,0 +1,59 @@ +import { test } from "node:test"; +import { + createMismatchEdgeCaseTests, + createMismatchRuleTests, +} from "../lib/test-templates.ts"; +import { + ACTOR_OUTBOX_PROPERTY_MISMATCH as ruleName, + default as rule, +} from "../rules/actor-outbox-property-mismatch.ts"; + +const config = { rule, ruleName }; + +// Standard mismatch rule tests +const tests = createMismatchRuleTests("outbox", config); +test( + `${ruleName}: ✅ Good - non-Federation object`, + tests["non-federation object"], +); +test( + `${ruleName}: ✅ Good - correct getter used`, + tests["correct getter used"], +); +test( + `${ruleName}: ✅ Good - property not present`, + tests["property not present"], +); +test( + `${ruleName}: ✅ Good - object literal with correct getter`, + tests["object literal with correct getter"], +); +test(`${ruleName}: ❌ Bad - wrong getter used`, tests["wrong getter used"]); +test( + `${ruleName}: ❌ Bad - wrong method in object literal`, + tests["wrong method in object literal"], +); +test(`${ruleName}: ❌ Bad - wrong identifier`, tests["wrong identifier"]); + +// Edge case tests +const edgeCases = createMismatchEdgeCaseTests("outbox", config); +test( + `${ruleName}: ✅ Edge - ternary with correct getter in both branches`, + edgeCases["ternary with correct getter in both branches"], +); +test( + `${ruleName}: ❌ Edge - ternary with wrong getter in consequent`, + edgeCases["ternary with wrong getter in consequent"], +); +test( + `${ruleName}: ❌ Edge - ternary with wrong getter in alternate`, + edgeCases["ternary with wrong getter in alternate"], +); +test( + `${ruleName}: ❌ Edge - ternary with wrong getter in both branches`, + edgeCases["ternary with wrong getter in both branches"], +); +test( + `${ruleName}: ✅ Edge - nested ternary with correct getter`, + edgeCases["nested ternary with correct getter"], +); diff --git a/packages/lint/src/tests/actor-outbox-property-required.test.ts b/packages/lint/src/tests/actor-outbox-property-required.test.ts new file mode 100644 index 000000000..e13828cdd --- /dev/null +++ b/packages/lint/src/tests/actor-outbox-property-required.test.ts @@ -0,0 +1,89 @@ +import { test } from "node:test"; +import { + createRequiredDispatcherRuleTests, + createRequiredEdgeCaseTests, +} from "../lib/test-templates.ts"; +import { + ACTOR_OUTBOX_PROPERTY_REQUIRED as ruleName, + default as rule, +} from "../rules/actor-outbox-property-required.ts"; + +const config = { rule, ruleName }; + +// Standard required dispatcher rule tests +const tests = createRequiredDispatcherRuleTests("outbox", config); +test( + `${ruleName}: ✅ Good - non-Federation object`, + tests["non-federation object"], +); +test( + `${ruleName}: ✅ Good - dispatcher NOT configured`, + tests["dispatcher not configured"], +); +test( + `${ruleName}: ✅ Good - dispatcher BEFORE (chained) with property`, + tests["dispatcher before chained with property"], +); +test( + `${ruleName}: ✅ Good - dispatcher BEFORE (separate) with property`, + tests["dispatcher before separate with property"], +); +test( + `${ruleName}: ✅ Good - dispatcher AFTER (chained) with property`, + tests["dispatcher after chained with property"], +); +test( + `${ruleName}: ✅ Good - dispatcher AFTER (separate) with property`, + tests["dispatcher after separate with property"], +); +test( + `${ruleName}: ✅ Good - object literal with property`, + tests["object literal with property"], +); +test( + `${ruleName}: ❌ Bad - dispatcher configured, property missing`, + tests["dispatcher configured property missing"], +); +test( + `${ruleName}: ❌ Bad - dispatcher BEFORE (separate), property missing`, + tests["dispatcher before separate property missing"], +); +test( + `${ruleName}: ❌ Bad - dispatcher AFTER (separate), property missing`, + tests["dispatcher after separate property missing"], +); +test( + `${ruleName}: ❌ Bad - object literal without property`, + tests["object literal without property"], +); +test( + `${ruleName}: ❌ Bad - variable assignment without property`, + tests["variable assignment without property"], +); + +// Edge case tests +const edgeCases = createRequiredEdgeCaseTests( + "outbox", + config, + "setOutboxDispatcher", +); +test( + `${ruleName}: ✅ Edge - ternary with property in both branches`, + edgeCases["ternary with property in both branches"], +); +test( + `${ruleName}: ❌ Edge - ternary missing property in consequent`, + edgeCases["ternary missing property in consequent"], +); +test( + `${ruleName}: ❌ Edge - ternary missing property in alternate`, + edgeCases["ternary missing property in alternate"], +); +test( + `${ruleName}: ❌ Edge - ternary missing property in both branches`, + edgeCases["ternary missing property in both branches"], +); +test( + `${ruleName}: ✅ Edge - nested ternary with property`, + edgeCases["nested ternary with property"], +); diff --git a/packages/lint/src/tests/actor-public-key-required.test.ts b/packages/lint/src/tests/actor-public-key-required.test.ts new file mode 100644 index 000000000..7994243a7 --- /dev/null +++ b/packages/lint/src/tests/actor-public-key-required.test.ts @@ -0,0 +1,211 @@ +import { test } from "node:test"; +import { actorKeyPropertyRequired } from "../lib/messages.ts"; +import { testDenoLint } from "../lib/test.ts"; +import { + ACTOR_PUBLIC_KEY_REQUIRED as ruleName, + default as rule, +} from "../rules/actor-public-key-required.ts"; + +test(`${ruleName}: ✅ Good - \`setActorDispatcher\` called on non-Federation object`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return new Person({ + name: "John Doe", + }); + }); + `, + rule, + ruleName, + federationSetup: ` + const federation = { + setActorDispatcher: () => {} + }; + `, + }); +}); + +test(`${ruleName}: ✅ Good - key pairs dispatcher NOT configured, property missing (no error)`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return { + id: ctx.getActorUri(identifier), + name: "Alice", + inbox: ctx.getInboxUri(identifier), + }; + }); + `, + rule, + ruleName, + }); +}); + +test(`${ruleName}: ✅ Good - key pairs dispatcher configured BEFORE setActorDispatcher, property present`, () => { + testDenoLint({ + code: ` + federation + .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + const keyPairs = await ctx.getActorKeyPairs(identifier); + return { + id: ctx.getActorUri(identifier), + publicKey: keyPairs[0].cryptographicKey, + }; + }) + .setKeyPairsDispatcher(async (ctx, identifier) => []); + `, + rule, + ruleName, + }); +}); + +test(`${ruleName}: ✅ Good - key pairs dispatcher configured BEFORE setActorDispatcher (separate calls), property present`, () => { + testDenoLint({ + code: ` + federation.setKeyPairsDispatcher(async (ctx, identifier) => []); + + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + const keyPairs = await ctx.getActorKeyPairs(identifier); + return { + id: ctx.getActorUri(identifier), + publicKey: keyPairs[0].cryptographicKey, + }; + }); + `, + rule, + ruleName, + }); +}); + +test(`${ruleName}: ✅ Good - key pairs dispatcher configured AFTER setActorDispatcher, property present`, () => { + testDenoLint({ + code: ` + federation + .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + const keyPairs = await ctx.getActorKeyPairs(identifier); + return { + id: ctx.getActorUri(identifier), + publicKey: keyPairs[0].cryptographicKey, + }; + }) + .setKeyPairsDispatcher(async (ctx, identifier) => []); + `, + rule, + ruleName, + }); +}); + +test(`${ruleName}: ✅ Good - key pairs dispatcher configured AFTER setActorDispatcher (separate calls), property present`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + const keyPairs = await ctx.getActorKeyPairs(identifier); + return { + id: ctx.getActorUri(identifier), + publicKey: keyPairs[0].cryptographicKey, + }; + }); + + federation.setKeyPairsDispatcher(async (ctx, identifier) => []); + `, + rule, + ruleName, + }); +}); + +test(`${ruleName}: ✅ Good - object literal with \`publicKey\``, () => { + testDenoLint({ + code: ` + federation + .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => ({ + id: ctx.getActorUri(identifier), + publicKey: await ctx.getActorKeyPairs(identifier).then(k => k[0].cryptographicKey), + })) + .setKeyPairsDispatcher(async (ctx, identifier) => []); + `, + rule, + ruleName, + }); +}); + +test(`${ruleName}: ❌ Bad - key pairs dispatcher configured BEFORE, property missing`, () => { + testDenoLint({ + code: ` + federation + .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return { + id: ctx.getActorUri(identifier), + name: "Alice", + }; + }) + .setKeyPairsDispatcher(async (ctx, identifier) => []); + `, + rule, + ruleName, + expectedError: actorKeyPropertyRequired("publicKey"), + }); +}); + +test(`${ruleName}: ❌ Bad - key pairs dispatcher configured BEFORE (separate calls), property missing`, () => { + testDenoLint({ + code: ` + federation.setKeyPairsDispatcher(async (ctx, identifier) => []); + + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return { + id: ctx.getActorUri(identifier), + name: "Alice", + }; + }); + `, + rule, + ruleName, + expectedError: actorKeyPropertyRequired("publicKey"), + }); +}); + +test(`${ruleName}: ❌ Bad - key pairs dispatcher configured AFTER, property missing`, () => { + testDenoLint({ + code: ` + federation + .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => ({ + id: ctx.getActorUri(identifier), + })) + .setKeyPairsDispatcher(async (ctx, identifier) => []); + `, + rule, + ruleName, + expectedError: actorKeyPropertyRequired("publicKey"), + }); +}); + +test(`${ruleName}: ❌ Bad - key pairs dispatcher configured AFTER (separate calls), property missing`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => ({ + id: ctx.getActorUri(identifier), + })); + + federation.setKeyPairsDispatcher(async (ctx, identifier) => []); + `, + rule, + ruleName, + expectedError: actorKeyPropertyRequired("publicKey"), + }); +}); + +test(`${ruleName}: ❌ Bad - object literal without property`, () => { + testDenoLint({ + code: ` + federation + .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => ({ + id: ctx.getActorUri(identifier), + name: "Alice", + })) + .setKeyPairsDispatcher(async (ctx, identifier) => []); + `, + rule, + ruleName, + expectedError: actorKeyPropertyRequired("publicKey"), + }); +}); diff --git a/packages/lint/src/tests/actor-shared-inbox-property-mismatch.test.ts b/packages/lint/src/tests/actor-shared-inbox-property-mismatch.test.ts new file mode 100644 index 000000000..7c982bf97 --- /dev/null +++ b/packages/lint/src/tests/actor-shared-inbox-property-mismatch.test.ts @@ -0,0 +1,59 @@ +import { test } from "node:test"; +import { + createMismatchEdgeCaseTests, + createMismatchRuleTests, +} from "../lib/test-templates.ts"; +import { + ACTOR_SHARED_INBOX_PROPERTY_MISMATCH as ruleName, + default as rule, +} from "../rules/actor-shared-inbox-property-mismatch.ts"; + +const config = { rule, ruleName }; + +// Standard mismatch rule tests +const tests = createMismatchRuleTests("sharedInbox", config); +test( + `${ruleName}: ✅ Good - non-Federation object`, + tests["non-federation object"], +); +test( + `${ruleName}: ✅ Good - correct getter used`, + tests["correct getter used"], +); +test( + `${ruleName}: ✅ Good - property not present`, + tests["property not present"], +); +test( + `${ruleName}: ✅ Good - object literal with correct getter`, + tests["object literal with correct getter"], +); +test(`${ruleName}: ❌ Bad - wrong getter used`, tests["wrong getter used"]); +test( + `${ruleName}: ❌ Bad - wrong method in object literal`, + tests["wrong method in object literal"], +); +test(`${ruleName}: ❌ Bad - wrong identifier`, tests["wrong identifier"]); + +// Edge case tests +const edgeCases = createMismatchEdgeCaseTests("sharedInbox", config); +test( + `${ruleName}: ✅ Edge - ternary with correct getter in both branches`, + edgeCases["ternary with correct getter in both branches"], +); +test( + `${ruleName}: ❌ Edge - ternary with wrong getter in consequent`, + edgeCases["ternary with wrong getter in consequent"], +); +test( + `${ruleName}: ❌ Edge - ternary with wrong getter in alternate`, + edgeCases["ternary with wrong getter in alternate"], +); +test( + `${ruleName}: ❌ Edge - ternary with wrong getter in both branches`, + edgeCases["ternary with wrong getter in both branches"], +); +test( + `${ruleName}: ✅ Edge - nested ternary with correct getter`, + edgeCases["nested ternary with correct getter"], +); diff --git a/packages/lint/src/tests/actor-shared-inbox-property-required.test.ts b/packages/lint/src/tests/actor-shared-inbox-property-required.test.ts new file mode 100644 index 000000000..b7334cd4d --- /dev/null +++ b/packages/lint/src/tests/actor-shared-inbox-property-required.test.ts @@ -0,0 +1,89 @@ +import { test } from "node:test"; +import { + createRequiredEdgeCaseTests, + createRequiredListenerRuleTests, +} from "../lib/test-templates.ts"; +import { + ACTOR_SHARED_INBOX_PROPERTY_REQUIRED as ruleName, + default as rule, +} from "../rules/actor-shared-inbox-property-required.ts"; + +const config = { rule, ruleName }; + +// Standard required listener rule tests +const tests = createRequiredListenerRuleTests("sharedInbox", config); +test( + `${ruleName}: ✅ Good - non-Federation object`, + tests["non-federation object"], +); +test( + `${ruleName}: ✅ Good - listeners NOT configured`, + tests["listeners not configured"], +); +test( + `${ruleName}: ✅ Good - listeners BEFORE (chained) with property`, + tests["listeners before chained with property"], +); +test( + `${ruleName}: ✅ Good - listeners BEFORE (separate) with property`, + tests["listeners before separate with property"], +); +test( + `${ruleName}: ✅ Good - listeners AFTER (chained) with property`, + tests["listeners after chained with property"], +); +test( + `${ruleName}: ✅ Good - listeners AFTER (separate) with property`, + tests["listeners after separate with property"], +); +test( + `${ruleName}: ✅ Good - object literal with property`, + tests["object literal with property"], +); +test( + `${ruleName}: ❌ Bad - listeners configured, property missing`, + tests["listeners configured property missing"], +); +test( + `${ruleName}: ❌ Bad - listeners BEFORE (separate), property missing`, + tests["listeners before separate property missing"], +); +test( + `${ruleName}: ❌ Bad - listeners AFTER (separate), property missing`, + tests["listeners after separate property missing"], +); +test( + `${ruleName}: ❌ Bad - object literal without property`, + tests["object literal without property"], +); +test( + `${ruleName}: ❌ Bad - variable assignment without property`, + tests["variable assignment without property"], +); + +// Edge case tests +const edgeCases = createRequiredEdgeCaseTests( + "sharedInbox", + config, + "setInboxListeners", +); +test( + `${ruleName}: ✅ Edge - ternary with property in both branches`, + edgeCases["ternary with property in both branches"], +); +test( + `${ruleName}: ❌ Edge - ternary missing property in consequent`, + edgeCases["ternary missing property in consequent"], +); +test( + `${ruleName}: ❌ Edge - ternary missing property in alternate`, + edgeCases["ternary missing property in alternate"], +); +test( + `${ruleName}: ❌ Edge - ternary missing property in both branches`, + edgeCases["ternary missing property in both branches"], +); +test( + `${ruleName}: ✅ Edge - nested ternary with property`, + edgeCases["nested ternary with property"], +); diff --git a/packages/lint/src/tests/collection-filtering-not-implemented.test.ts b/packages/lint/src/tests/collection-filtering-not-implemented.test.ts new file mode 100644 index 000000000..6270c239b --- /dev/null +++ b/packages/lint/src/tests/collection-filtering-not-implemented.test.ts @@ -0,0 +1,127 @@ +import { test } from "node:test"; +import { properties } from "../lib/const.ts"; +import { COLLECTION_FILTERING_NOT_IMPLEMENTED_ERROR as expectedError } from "../lib/messages.ts"; +import { testDenoLint } from "../lib/test.ts"; +import { + COLLECTION_FILTERING_NOT_IMPLEMENTED as ruleName, + default as rule, +} from "../rules/collection-filtering-not-implemented.ts"; + +const filterless = ["ctx", "identifier", "cursor"] as const; + +test( + `${ruleName}: ✅ Good - async arrow function with filter parameter`, + createTestCode(), +); + +test( + `${ruleName}: ✅ Good - async function expression with filter`, + createTestCode({ arrow: false }), +); + +test( + `${ruleName}: ✅ Good - sync arrow function with filter`, + createTestCode({ async: false }), +); + +test( + `${ruleName}: ✅ Good - sync function expression with filter`, + createTestCode({ async: false, arrow: false }), +); + +test( + `${ruleName}: ❌ Bad - async arrow function without filter parameter`, + createTestCode({ params: filterless }, expectedError), +); + +test( + `${ruleName}: ❌ Bad - async function expression without filter`, + createTestCode({ params: filterless, arrow: false }, expectedError), +); + +test( + `${ruleName}: ❌ Bad - sync arrow function expression without filter`, + createTestCode({ params: filterless, async: false }, expectedError), +); + +test( + `${ruleName}: ❌ Bad - sync function expression without filter`, + createTestCode( + { params: filterless, async: false, arrow: false }, + expectedError, + ), +); + +test( + `${ruleName}: ✅ Good - 4th parameter but unnamed filter`, + createTestCode({ params: ["ctx", "identifier", "cursor", "somethingElse"] }), +); + +test( + `${ruleName}: ❌ Bad - only two parameters (missing cursor and filter)`, + createTestCode({ params: ["ctx", "identifier"] }, expectedError), +); + +test(`${ruleName}: ✅ Good - non-federation object is not checked`, () => + filterNeeded.forEach((name) => + testDenoLint({ + code: createDispatcherCode(name, { params: filterless }), + rule, + ruleName, + federationSetup: ` + const federation = { + ${properties[name].setter}: () => {} + }; + `, + }) + )); + +function createTestCode( + codeOptions: Parameters[1] = {}, + expectedError?: string, +) { + return () => + filterNeeded.forEach((name) => + testDenoLint({ + code: createDispatcherCode(name, codeOptions), + rule, + ruleName, + expectedError, + }) + ); +} + +const filterNeeded = [ + "followers", + "following", + "outbox", + "liked", + "featured", + "featuredTags", +] as const satisfies (keyof typeof properties)[]; + +const createDispatcherCode = ( + name: keyof typeof properties, + { + params = ["ctx", "identifier", "cursor", "filter"], + async = true, + arrow = true, + }: { + params?: readonly string[]; + async?: boolean; + arrow?: boolean; + } = {}, +): string => { + const paramsString = params.join(", "); + const asyncKeyword = async ? "async" : ""; + const [funcKeyword, arrowSymbol] = arrow ? ["", "=>"] : ["function", ""]; + + return ` + federation.${properties[name].setter}( + "/users/{identifier}/${name}", + ${asyncKeyword} ${funcKeyword}(${paramsString}) ${arrowSymbol} { + return { items: [] }; + } + ); + `; +}; diff --git a/packages/lint/src/tests/edge-cases.test.ts b/packages/lint/src/tests/edge-cases.test.ts new file mode 100644 index 000000000..168b831e0 --- /dev/null +++ b/packages/lint/src/tests/edge-cases.test.ts @@ -0,0 +1,217 @@ +import { test } from "node:test"; +import { properties } from "../lib/const.ts"; +import { actorPropertyRequired } from "../lib/messages.ts"; +import { testDenoLint } from "../lib/test.ts"; +import { + ACTOR_ID_REQUIRED as ruleName, + default as rule, +} from "../rules/actor-id-required.ts"; + +const expectedError = actorPropertyRequired(properties.id.name); + +const actorProperties = [ + "id", + "following", + "followers", + "outbox", + "inbox", + "liked", + "featured", + "featuredTags", +] as const satisfies (keyof typeof properties)[]; + +const createActorDispatcherCode = ( + name: keyof typeof properties, + { + returnCode, + preReturnCode = "", + }: { + returnCode: string; + preReturnCode?: string; + }, +): string => ` + federation.${properties.id.setter}("/users/{identifier}", async (ctx, identifier) => { + ${preReturnCode} + return ${returnCode}; + }); +`; + +const withId = (extra = "") => + `new Person({ id: ctx.getActorUri(identifier), name: "User"${extra} })`; +const withoutId = () => `new Person({ name: "User" })`; + +const createTestCode = ( + returnCode: string, + preReturnCode = "", + error?: string, +) => +() => + actorProperties.forEach((name) => + testDenoLint({ + code: createActorDispatcherCode(name, { returnCode, preReturnCode }), + rule, + ruleName, + expectedError: error, + }) + ); + +test( + `${ruleName}: ✅ Good - multiple return statements - all have id`, + createTestCode( + ` + identifier === "admin" + ? ${withId()} + : ${withId()} + `.trim().replace(/\n\s*/g, " "), + ` + if (identifier === "admin") { + return ${withId()}; + } + `, + ), +); + +test( + `${ruleName}: ✅ Good - multiple return statements - first missing id (known limitation)`, + createTestCode( + withId(), + ` + if (identifier === "admin") { + return ${withoutId()}; + } + `, + ), +); + +test( + `${ruleName}: ❌ Bad - multiple return statements - second missing id`, + createTestCode( + withoutId(), + ` + if (identifier === "admin") { + return ${withId()}; + } + `, + expectedError, + ), +); + +test( + `${ruleName}: ✅ Good - if/else with else block (known limitation: else not checked)`, + createTestCode( + withId(), + ` + if (identifier) { + return ${withId()}; + } else { + return ${withoutId()}; + } + `, + ), +); + +test( + `${ruleName}: ✅ Good - nested if with id`, + createTestCode( + withId(), + ` + if (identifier) { + if (identifier === "admin") { + return ${withId()}; + } + return ${withId()}; + } + `, + ), +); + +test( + `${ruleName}: ✅ Good - ternary operator with id in both branches`, + createTestCode(`identifier ? ${withId()} : ${withId()}`, ""), +); + +test( + `${ruleName}: ❌ Bad - ternary operator without id in consequent`, + createTestCode( + `identifier ? ${withoutId()} : ${withId()}`, + "", + expectedError, + ), +); + +test( + `${ruleName}: ❌ Bad - ternary operator without id in alternate`, + createTestCode( + `identifier ? ${withId()} : ${withoutId()}`, + "", + expectedError, + ), +); + +test( + `${ruleName}: ✅ Good - spread operator with id property after spread`, + createTestCode( + `new Person({ ...base, id: ctx.getActorUri(identifier) })`, + `const base = { name: "User" };`, + ), +); + +test( + `${ruleName}: ❌ Bad - spread operator with id in spread source (known limitation)`, + createTestCode( + `new Person({ ...base })`, + `const base = { id: ctx.getActorUri(identifier), name: "User" };`, + expectedError, + ), +); + +test( + `${ruleName}: ❌ Bad - variable assignment then return (known limitation)`, + createTestCode( + `actor`, + `const actor = ${withId()};`, + expectedError, + ), +); + +test( + `${ruleName}: ❌ Bad - property assignment after construction (known limitation)`, + createTestCode( + `actor`, + `const actor = ${withoutId()};\n actor.id = ctx.getActorUri(identifier);`, + expectedError, + ), +); + +test(`${ruleName}: ❌ Bad - arrow function direct return NewExpression (known limitation)`, () => + actorProperties.forEach((name) => + testDenoLint({ + code: ` + federation.${properties.id.setter}("/users/{identifier}", async (ctx, identifier) => + ${withId()} + ); + `, + rule, + ruleName, + expectedError, + }) + )); + +test(`${ruleName}: ✅ Good - arrow function direct return with object literal`, () => + actorProperties.forEach((name) => + testDenoLint({ + code: ` + federation.${properties.id.setter}("/users/{identifier}", async (ctx, identifier) => ({ + id: ctx.getActorUri(identifier), + name: "User", + })); + `, + rule, + ruleName, + }) + )); + +test( + `${ruleName}: ✅ Good - return null (no actor)`, + createTestCode(withId(), `if (!identifier) return null;`), +); From ea5c2d269d78a429378438af5ad449f7dff16bf8 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Mon, 8 Dec 2025 04:52:49 +0000 Subject: [PATCH 15/41] Removed object literal test --- packages/lint/src/lib/test-templates.ts | 203 ++---------------- .../actor-assertion-method-required.test.ts | 40 ---- .../actor-featured-property-mismatch.test.ts | 8 - .../actor-featured-property-required.test.ts | 8 - ...or-featured-tags-property-mismatch.test.ts | 8 - ...or-featured-tags-property-required.test.ts | 8 - .../actor-followers-property-mismatch.test.ts | 8 - .../actor-followers-property-required.test.ts | 8 - .../actor-following-property-mismatch.test.ts | 8 - .../actor-following-property-required.test.ts | 8 - .../lint/src/tests/actor-id-mismatch.test.ts | 51 ++--- .../lint/src/tests/actor-id-required.test.ts | 30 --- .../actor-inbox-property-mismatch.test.ts | 8 - .../actor-inbox-property-required.test.ts | 8 - .../actor-liked-property-mismatch.test.ts | 8 - .../actor-liked-property-required.test.ts | 8 - .../actor-outbox-property-mismatch.test.ts | 8 - .../actor-outbox-property-required.test.ts | 8 - .../tests/actor-public-key-required.test.ts | 75 +++---- ...tor-shared-inbox-property-mismatch.test.ts | 8 - ...tor-shared-inbox-property-required.test.ts | 8 - packages/lint/src/tests/edge-cases.test.ts | 14 -- 22 files changed, 59 insertions(+), 482 deletions(-) diff --git a/packages/lint/src/lib/test-templates.ts b/packages/lint/src/lib/test-templates.ts index e1bec9394..2553af811 100644 --- a/packages/lint/src/lib/test-templates.ts +++ b/packages/lint/src/lib/test-templates.ts @@ -53,7 +53,6 @@ const createSeparateDispatcherCode = ( }; const createTestCode = ( - type: "class" | "literal", propertyKey: PropertyKey, includeProperty: boolean, ) => { @@ -70,18 +69,11 @@ const createTestCode = ( } } - if (type === "class") { - return `return new Person({ - id: ctx.getActorUri(identifier), - ${propertyCode} - name: "John Doe", - });`; - } - return `return { + return `return new Person({ id: ctx.getActorUri(identifier), ${propertyCode} name: "John Doe", - };`; + });`; }; // ============================================================================= @@ -104,7 +96,7 @@ export function createRequiredDispatcherRuleTests( // ✅ Good - non-Federation object "non-federation object": () => { testDenoLint({ - code: createDispatcherCode(createTestCode("class", propertyKey, false)), + code: createDispatcherCode(createTestCode(propertyKey, false)), rule, ruleName, federationSetup: ` @@ -116,7 +108,7 @@ export function createRequiredDispatcherRuleTests( // ✅ Good - dispatcher NOT configured "dispatcher not configured": () => { testDenoLint({ - code: createDispatcherCode(createTestCode("class", propertyKey, false)), + code: createDispatcherCode(createTestCode(propertyKey, false)), rule, ruleName, }); @@ -126,7 +118,7 @@ export function createRequiredDispatcherRuleTests( "dispatcher before chained with property": () => { testDenoLint({ code: createChainedDispatcherCode( - createTestCode("class", propertyKey, true), + createTestCode(propertyKey, true), prop.setter, ), rule, @@ -138,7 +130,7 @@ export function createRequiredDispatcherRuleTests( "dispatcher before separate with property": () => { testDenoLint({ code: createSeparateDispatcherCode( - createTestCode("class", propertyKey, true), + createTestCode(propertyKey, true), prop.setter, true, ), @@ -151,7 +143,7 @@ export function createRequiredDispatcherRuleTests( "dispatcher after chained with property": () => { testDenoLint({ code: createChainedDispatcherCode( - createTestCode("class", propertyKey, true), + createTestCode(propertyKey, true), prop.setter, ), rule, @@ -163,7 +155,7 @@ export function createRequiredDispatcherRuleTests( "dispatcher after separate with property": () => { testDenoLint({ code: createSeparateDispatcherCode( - createTestCode("class", propertyKey, true), + createTestCode(propertyKey, true), prop.setter, false, ), @@ -172,23 +164,11 @@ export function createRequiredDispatcherRuleTests( }); }, - // ✅ Good - object literal with property - "object literal with property": () => { - testDenoLint({ - code: createChainedDispatcherCode( - createTestCode("literal", propertyKey, true), - prop.setter, - ), - rule, - ruleName, - }); - }, - // ❌ Bad - dispatcher configured, property missing "dispatcher configured property missing": () => { testDenoLint({ code: createChainedDispatcherCode( - createTestCode("class", propertyKey, false), + createTestCode(propertyKey, false), prop.setter, ), rule, @@ -201,7 +181,7 @@ export function createRequiredDispatcherRuleTests( "dispatcher before separate property missing": () => { testDenoLint({ code: createSeparateDispatcherCode( - createTestCode("class", propertyKey, false), + createTestCode(propertyKey, false), prop.setter, true, ), @@ -215,7 +195,7 @@ export function createRequiredDispatcherRuleTests( "dispatcher after separate property missing": () => { testDenoLint({ code: createSeparateDispatcherCode( - createTestCode("class", propertyKey, false), + createTestCode(propertyKey, false), prop.setter, false, ), @@ -225,19 +205,6 @@ export function createRequiredDispatcherRuleTests( }); }, - // ❌ Bad - object literal without property - "object literal without property": () => { - testDenoLint({ - code: createChainedDispatcherCode( - createTestCode("literal", propertyKey, false), - prop.setter, - ), - rule, - ruleName, - expectedError, - }); - }, - // ❌ Bad - variable assignment without property "variable assignment without property": () => { testDenoLint({ @@ -275,30 +242,20 @@ export function createRequiredListenerRuleTests( return "inbox: ctx.getInboxUri(identifier),"; }; - const createActorCode = ( - type: "class" | "literal", - includeProperty: boolean, - ) => { + const createActorCode = (includeProperty: boolean) => { const propCode = createPropertyCode(includeProperty); - if (type === "class") { - return `return new Person({ - id: ctx.getActorUri(identifier), - ${propCode} - name: "John Doe", - });`; - } - return `return { + return `return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "John Doe", - };`; + });`; }; return { // ✅ Good - non-Federation object "non-federation object": () => { testDenoLint({ - code: createDispatcherCode(createActorCode("class", false)), + code: createDispatcherCode(createActorCode(false)), rule, ruleName, federationSetup: ` @@ -310,7 +267,7 @@ export function createRequiredListenerRuleTests( // ✅ Good - listeners NOT configured "listeners not configured": () => { testDenoLint({ - code: createDispatcherCode(createActorCode("class", false)), + code: createDispatcherCode(createActorCode(false)), rule, ruleName, }); @@ -320,7 +277,7 @@ export function createRequiredListenerRuleTests( "listeners before chained with property": () => { testDenoLint({ code: createChainedDispatcherCode( - createActorCode("class", true), + createActorCode(true), "setInboxListeners", ), rule, @@ -332,7 +289,7 @@ export function createRequiredListenerRuleTests( "listeners before separate with property": () => { testDenoLint({ code: createSeparateDispatcherCode( - createActorCode("class", true), + createActorCode(true), "setInboxListeners", true, ), @@ -345,7 +302,7 @@ export function createRequiredListenerRuleTests( "listeners after chained with property": () => { testDenoLint({ code: createChainedDispatcherCode( - createActorCode("class", true), + createActorCode(true), "setInboxListeners", ), rule, @@ -357,7 +314,7 @@ export function createRequiredListenerRuleTests( "listeners after separate with property": () => { testDenoLint({ code: createSeparateDispatcherCode( - createActorCode("class", true), + createActorCode(true), "setInboxListeners", false, ), @@ -366,23 +323,11 @@ export function createRequiredListenerRuleTests( }); }, - // ✅ Good - object literal with property - "object literal with property": () => { - testDenoLint({ - code: createChainedDispatcherCode( - createActorCode("literal", true), - "setInboxListeners", - ), - rule, - ruleName, - }); - }, - // ❌ Bad - listeners configured, property missing "listeners configured property missing": () => { testDenoLint({ code: createChainedDispatcherCode( - createActorCode("class", false), + createActorCode(false), "setInboxListeners", ), rule, @@ -395,7 +340,7 @@ export function createRequiredListenerRuleTests( "listeners before separate property missing": () => { testDenoLint({ code: createSeparateDispatcherCode( - createActorCode("class", false), + createActorCode(false), "setInboxListeners", true, ), @@ -409,7 +354,7 @@ export function createRequiredListenerRuleTests( "listeners after separate property missing": () => { testDenoLint({ code: createSeparateDispatcherCode( - createActorCode("class", false), + createActorCode(false), "setInboxListeners", false, ), @@ -419,19 +364,6 @@ export function createRequiredListenerRuleTests( }); }, - // ❌ Bad - object literal without property - "object literal without property": () => { - testDenoLint({ - code: createChainedDispatcherCode( - createActorCode("literal", false), - "setInboxListeners", - ), - rule, - ruleName, - expectedError, - }); - }, - // ❌ Bad - variable assignment without property "variable assignment without property": () => { testDenoLint({ @@ -495,18 +427,6 @@ export function createIdRequiredRuleTests(config: TestConfig) { }); }, - // ✅ Good - object literal with id - "object literal with id": () => { - testDenoLint({ - code: createDispatcherCode(`return { - id: "https://example.com/users/123", - name: "John Doe", - };`), - rule, - ruleName, - }); - }, - // ✅ Good - BlockStatement with id "block statement with id": () => { testDenoLint({ @@ -566,16 +486,6 @@ export function createIdRequiredRuleTests(config: TestConfig) { expectedError, }); }, - - // ❌ Bad - object literal without id - "object literal without id": () => { - testDenoLint({ - code: createDispatcherCode(`return { name: "John Doe" };`), - rule, - ruleName, - expectedError, - }); - }, }; } @@ -672,23 +582,6 @@ export function createKeyRequiredRuleTests( }); }, - // ✅ Good - object literal with property - "object literal with property": () => { - testDenoLint({ - code: createSeparateDispatcherCode( - `return { - id: ctx.getActorUri(identifier), - ${propertyName}: ctx.getActorKeyPairs(identifier), - name: "John Doe", - };`, - "setKeyPairsDispatcher", - true, - ), - rule, - ruleName, - }); - }, - // ❌ Bad - key pairs dispatcher configured BEFORE (separate), property missing "key pairs before separate property missing": () => { testDenoLint({ @@ -742,23 +635,6 @@ export function createKeyRequiredRuleTests( expectedError, }); }, - - // ❌ Bad - object literal without property - "object literal without property": () => { - testDenoLint({ - code: createSeparateDispatcherCode( - `return { - id: ctx.getActorUri(identifier), - name: "John Doe", - };`, - "setKeyPairsDispatcher", - true, - ), - rule, - ruleName, - expectedError, - }); - }, }; } @@ -836,23 +712,6 @@ export function createMismatchRuleTests( }); }, - // ❌ Bad - wrong method in object literal - "wrong method in object literal": () => { - const propCode = isNested - ? `endpoints: { sharedInbox: ctx.${wrongGetter}() },` - : `${prop.name}: ctx.${wrongGetter}(identifier),`; - testDenoLint({ - code: createDispatcherCode(`return { - id: ctx.getActorUri(identifier), - ${propCode} - name: "John Doe", - };`), - rule, - ruleName, - expectedError, - }); - }, - // ❌ Bad - wrong identifier "wrong identifier": () => { const propCode = isNested @@ -881,22 +740,6 @@ export function createMismatchRuleTests( ruleName, }); }, - - // ✅ Good - object literal with correct getter - "object literal with correct getter": () => { - const propCode = isNested - ? `endpoints: { sharedInbox: ctx.${prop.getter}() },` - : `${prop.name}: ctx.${prop.getter}(identifier),`; - testDenoLint({ - code: createDispatcherCode(`return { - id: ctx.getActorUri(identifier), - ${propCode} - name: "John Doe", - };`), - rule, - ruleName, - }); - }, }; } diff --git a/packages/lint/src/tests/actor-assertion-method-required.test.ts b/packages/lint/src/tests/actor-assertion-method-required.test.ts index b6abc6318..d270f6758 100644 --- a/packages/lint/src/tests/actor-assertion-method-required.test.ts +++ b/packages/lint/src/tests/actor-assertion-method-required.test.ts @@ -116,26 +116,6 @@ test(`${ruleName}: ✅ Good - key pairs dispatcher configured AFTER setActorDisp }); }); -test(`${ruleName}: ✅ Good - object literal with \`assertionMethod\``, () => { - testDenoLint({ - code: ` - federation.setKeyPairsDispatcher(async (ctx, identifier) => { - return []; - }); - - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return { - id: ctx.getActorUri(identifier), - assertionMethod: ctx.getActorKeyPairs(identifier), - name: "John Doe", - }; - }); - `, - rule, - ruleName, - }); -}); - test(`${ruleName}: ❌ Bad - key pairs dispatcher configured BEFORE, property missing`, () => { testDenoLint({ code: ` @@ -211,23 +191,3 @@ test(`${ruleName}: ❌ Bad - key pairs dispatcher configured AFTER (chained), pr expectedError: actorKeyPropertyRequired("assertionMethod"), }); }); - -test(`${ruleName}: ❌ Bad - object literal without property`, () => { - testDenoLint({ - code: ` - federation.setKeyPairsDispatcher(async (ctx, identifier) => { - return []; - }); - - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return { - id: ctx.getActorUri(identifier), - name: "John Doe", - }; - }); - `, - rule, - ruleName, - expectedError: actorKeyPropertyRequired("assertionMethod"), - }); -}); diff --git a/packages/lint/src/tests/actor-featured-property-mismatch.test.ts b/packages/lint/src/tests/actor-featured-property-mismatch.test.ts index e3f9d9159..20d112d82 100644 --- a/packages/lint/src/tests/actor-featured-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-featured-property-mismatch.test.ts @@ -24,15 +24,7 @@ test( `${ruleName}: ✅ Good - property not present`, tests["property not present"], ); -test( - `${ruleName}: ✅ Good - object literal with correct getter`, - tests["object literal with correct getter"], -); test(`${ruleName}: ❌ Bad - wrong getter used`, tests["wrong getter used"]); -test( - `${ruleName}: ❌ Bad - wrong method in object literal`, - tests["wrong method in object literal"], -); test(`${ruleName}: ❌ Bad - wrong identifier`, tests["wrong identifier"]); // Edge case tests diff --git a/packages/lint/src/tests/actor-featured-property-required.test.ts b/packages/lint/src/tests/actor-featured-property-required.test.ts index f4d233d3a..eeb79ebc8 100644 --- a/packages/lint/src/tests/actor-featured-property-required.test.ts +++ b/packages/lint/src/tests/actor-featured-property-required.test.ts @@ -36,10 +36,6 @@ test( `${ruleName}: ✅ Good - dispatcher AFTER (separate) with property`, tests["dispatcher after separate with property"], ); -test( - `${ruleName}: ✅ Good - object literal with property`, - tests["object literal with property"], -); test( `${ruleName}: ❌ Bad - dispatcher configured, property missing`, tests["dispatcher configured property missing"], @@ -52,10 +48,6 @@ test( `${ruleName}: ❌ Bad - dispatcher AFTER (separate), property missing`, tests["dispatcher after separate property missing"], ); -test( - `${ruleName}: ❌ Bad - object literal without property`, - tests["object literal without property"], -); test( `${ruleName}: ❌ Bad - variable assignment without property`, tests["variable assignment without property"], diff --git a/packages/lint/src/tests/actor-featured-tags-property-mismatch.test.ts b/packages/lint/src/tests/actor-featured-tags-property-mismatch.test.ts index f3e5e2eaa..fe31d857d 100644 --- a/packages/lint/src/tests/actor-featured-tags-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-featured-tags-property-mismatch.test.ts @@ -24,15 +24,7 @@ test( `${ruleName}: ✅ Good - property not present`, tests["property not present"], ); -test( - `${ruleName}: ✅ Good - object literal with correct getter`, - tests["object literal with correct getter"], -); test(`${ruleName}: ❌ Bad - wrong getter used`, tests["wrong getter used"]); -test( - `${ruleName}: ❌ Bad - wrong method in object literal`, - tests["wrong method in object literal"], -); test(`${ruleName}: ❌ Bad - wrong identifier`, tests["wrong identifier"]); // Edge case tests diff --git a/packages/lint/src/tests/actor-featured-tags-property-required.test.ts b/packages/lint/src/tests/actor-featured-tags-property-required.test.ts index 016c29862..79acb25fe 100644 --- a/packages/lint/src/tests/actor-featured-tags-property-required.test.ts +++ b/packages/lint/src/tests/actor-featured-tags-property-required.test.ts @@ -36,10 +36,6 @@ test( `${ruleName}: ✅ Good - dispatcher AFTER (separate) with property`, tests["dispatcher after separate with property"], ); -test( - `${ruleName}: ✅ Good - object literal with property`, - tests["object literal with property"], -); test( `${ruleName}: ❌ Bad - dispatcher configured, property missing`, tests["dispatcher configured property missing"], @@ -52,10 +48,6 @@ test( `${ruleName}: ❌ Bad - dispatcher AFTER (separate), property missing`, tests["dispatcher after separate property missing"], ); -test( - `${ruleName}: ❌ Bad - object literal without property`, - tests["object literal without property"], -); test( `${ruleName}: ❌ Bad - variable assignment without property`, tests["variable assignment without property"], diff --git a/packages/lint/src/tests/actor-followers-property-mismatch.test.ts b/packages/lint/src/tests/actor-followers-property-mismatch.test.ts index 17fa972ad..9a2eea06a 100644 --- a/packages/lint/src/tests/actor-followers-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-followers-property-mismatch.test.ts @@ -24,15 +24,7 @@ test( `${ruleName}: ✅ Good - property not present`, tests["property not present"], ); -test( - `${ruleName}: ✅ Good - object literal with correct getter`, - tests["object literal with correct getter"], -); test(`${ruleName}: ❌ Bad - wrong getter used`, tests["wrong getter used"]); -test( - `${ruleName}: ❌ Bad - wrong method in object literal`, - tests["wrong method in object literal"], -); test(`${ruleName}: ❌ Bad - wrong identifier`, tests["wrong identifier"]); // Edge case tests diff --git a/packages/lint/src/tests/actor-followers-property-required.test.ts b/packages/lint/src/tests/actor-followers-property-required.test.ts index b57e6886f..a8c5f6a98 100644 --- a/packages/lint/src/tests/actor-followers-property-required.test.ts +++ b/packages/lint/src/tests/actor-followers-property-required.test.ts @@ -36,10 +36,6 @@ test( `${ruleName}: ✅ Good - dispatcher AFTER (separate) with property`, tests["dispatcher after separate with property"], ); -test( - `${ruleName}: ✅ Good - object literal with property`, - tests["object literal with property"], -); test( `${ruleName}: ❌ Bad - dispatcher configured, property missing`, tests["dispatcher configured property missing"], @@ -52,10 +48,6 @@ test( `${ruleName}: ❌ Bad - dispatcher AFTER (separate), property missing`, tests["dispatcher after separate property missing"], ); -test( - `${ruleName}: ❌ Bad - object literal without property`, - tests["object literal without property"], -); test( `${ruleName}: ❌ Bad - variable assignment without property`, tests["variable assignment without property"], diff --git a/packages/lint/src/tests/actor-following-property-mismatch.test.ts b/packages/lint/src/tests/actor-following-property-mismatch.test.ts index d43aa89dd..b741156b9 100644 --- a/packages/lint/src/tests/actor-following-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-following-property-mismatch.test.ts @@ -24,15 +24,7 @@ test( `${ruleName}: ✅ Good - property not present`, tests["property not present"], ); -test( - `${ruleName}: ✅ Good - object literal with correct getter`, - tests["object literal with correct getter"], -); test(`${ruleName}: ❌ Bad - wrong getter used`, tests["wrong getter used"]); -test( - `${ruleName}: ❌ Bad - wrong method in object literal`, - tests["wrong method in object literal"], -); test(`${ruleName}: ❌ Bad - wrong identifier`, tests["wrong identifier"]); // Edge case tests diff --git a/packages/lint/src/tests/actor-following-property-required.test.ts b/packages/lint/src/tests/actor-following-property-required.test.ts index ac3b6b196..0c9c2ea91 100644 --- a/packages/lint/src/tests/actor-following-property-required.test.ts +++ b/packages/lint/src/tests/actor-following-property-required.test.ts @@ -36,10 +36,6 @@ test( `${ruleName}: ✅ Good - dispatcher AFTER (separate) with property`, tests["dispatcher after separate with property"], ); -test( - `${ruleName}: ✅ Good - object literal with property`, - tests["object literal with property"], -); test( `${ruleName}: ❌ Bad - dispatcher configured, property missing`, tests["dispatcher configured property missing"], @@ -52,10 +48,6 @@ test( `${ruleName}: ❌ Bad - dispatcher AFTER (separate), property missing`, tests["dispatcher after separate property missing"], ); -test( - `${ruleName}: ❌ Bad - object literal without property`, - tests["object literal without property"], -); test( `${ruleName}: ❌ Bad - variable assignment without property`, tests["variable assignment without property"], diff --git a/packages/lint/src/tests/actor-id-mismatch.test.ts b/packages/lint/src/tests/actor-id-mismatch.test.ts index 615ed2cc6..18257f00a 100644 --- a/packages/lint/src/tests/actor-id-mismatch.test.ts +++ b/packages/lint/src/tests/actor-id-mismatch.test.ts @@ -41,21 +41,6 @@ test(`${ruleName}: ✅ Good - id uses ctx.getActorUri(identifier)`, () => { }); }); -test(`${ruleName}: ✅ Good - object literal with correct id`, () => { - testDenoLint({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return { - id: ctx.getActorUri(identifier), - name: "John Doe", - }; - }); - `, - rule, - ruleName, - }); -}); - test(`${ruleName}: ✅ Good - BlockStatement with correct id`, () => { testDenoLint({ code: ` @@ -120,22 +105,6 @@ test(`${ruleName}: ❌ Bad - id uses wrong identifier parameter`, () => { }); }); -test(`${ruleName}: ❌ Bad - object literal with wrong id`, () => { - testDenoLint({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return { - id: "https://example.com/users/123", - name: "John Doe", - }; - }); - `, - rule, - ruleName, - expectedError: actorPropertyMismatch("id", "ctx.getActorUri(identifier)"), - }); -}); - test(`${ruleName} Edge: ✅ multiple return statements - all correct`, () => { testDenoLint({ code: ` @@ -198,10 +167,12 @@ test(`${ruleName} Edge: ❌ spread operator with wrong id after spread`, () => { test(`${ruleName} Edge: ✅ arrow function direct return with correct id`, () => { testDenoLint({ code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => ({ - id: ctx.getActorUri(identifier), - name: "User", - })); + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + name: "User", + }); + }); `, rule, ruleName, @@ -211,10 +182,12 @@ test(`${ruleName} Edge: ✅ arrow function direct return with correct id`, () => test(`${ruleName} Edge: ❌ arrow function direct return with wrong id`, () => { testDenoLint({ code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => ({ - id: "hardcoded", - name: "User", - })); + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return new Person({ + id: "hardcoded", + name: "User", + }); + }); `, rule, ruleName, diff --git a/packages/lint/src/tests/actor-id-required.test.ts b/packages/lint/src/tests/actor-id-required.test.ts index 0d018345d..7af9fd260 100644 --- a/packages/lint/src/tests/actor-id-required.test.ts +++ b/packages/lint/src/tests/actor-id-required.test.ts @@ -56,21 +56,6 @@ test(`${ruleName}: ✅ Good - with \`id\` property using ctx.getActorUri()`, () }); }); -test(`${ruleName}: ✅ Good - object literal with \`id\``, () => { - testDenoLint({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return { - id: "https://example.com/users/123", - name: "John Doe", - }; - }); - `, - rule, - ruleName, - }); -}); - test(`${ruleName}: ✅ Good - BlockStatement with \`id\``, () => { testDenoLint({ code: ` @@ -147,18 +132,3 @@ test(`${ruleName}: ❌ Bad - variable assignment without \`id\``, () => { expectedError: actorPropertyRequired(properties.id.name), }); }); - -test(`${ruleName}: ❌ Bad - object literal without \`id\``, () => { - testDenoLint({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return { - name: "John Doe", - }; - }); - `, - rule, - ruleName, - expectedError: actorPropertyRequired(properties.id.name), - }); -}); diff --git a/packages/lint/src/tests/actor-inbox-property-mismatch.test.ts b/packages/lint/src/tests/actor-inbox-property-mismatch.test.ts index 5fc13d47d..9ff6c5894 100644 --- a/packages/lint/src/tests/actor-inbox-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-inbox-property-mismatch.test.ts @@ -24,15 +24,7 @@ test( `${ruleName}: ✅ Good - property not present`, tests["property not present"], ); -test( - `${ruleName}: ✅ Good - object literal with correct getter`, - tests["object literal with correct getter"], -); test(`${ruleName}: ❌ Bad - wrong getter used`, tests["wrong getter used"]); -test( - `${ruleName}: ❌ Bad - wrong method in object literal`, - tests["wrong method in object literal"], -); test(`${ruleName}: ❌ Bad - wrong identifier`, tests["wrong identifier"]); // Edge case tests diff --git a/packages/lint/src/tests/actor-inbox-property-required.test.ts b/packages/lint/src/tests/actor-inbox-property-required.test.ts index e36b53a7c..f42cf7e2b 100644 --- a/packages/lint/src/tests/actor-inbox-property-required.test.ts +++ b/packages/lint/src/tests/actor-inbox-property-required.test.ts @@ -36,10 +36,6 @@ test( `${ruleName}: ✅ Good - listeners AFTER (separate) with property`, tests["listeners after separate with property"], ); -test( - `${ruleName}: ✅ Good - object literal with property`, - tests["object literal with property"], -); test( `${ruleName}: ❌ Bad - listeners configured, property missing`, tests["listeners configured property missing"], @@ -52,10 +48,6 @@ test( `${ruleName}: ❌ Bad - listeners AFTER (separate), property missing`, tests["listeners after separate property missing"], ); -test( - `${ruleName}: ❌ Bad - object literal without property`, - tests["object literal without property"], -); test( `${ruleName}: ❌ Bad - variable assignment without property`, tests["variable assignment without property"], diff --git a/packages/lint/src/tests/actor-liked-property-mismatch.test.ts b/packages/lint/src/tests/actor-liked-property-mismatch.test.ts index cdead140f..62a3a5007 100644 --- a/packages/lint/src/tests/actor-liked-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-liked-property-mismatch.test.ts @@ -24,15 +24,7 @@ test( `${ruleName}: ✅ Good - property not present`, tests["property not present"], ); -test( - `${ruleName}: ✅ Good - object literal with correct getter`, - tests["object literal with correct getter"], -); test(`${ruleName}: ❌ Bad - wrong getter used`, tests["wrong getter used"]); -test( - `${ruleName}: ❌ Bad - wrong method in object literal`, - tests["wrong method in object literal"], -); test(`${ruleName}: ❌ Bad - wrong identifier`, tests["wrong identifier"]); // Edge case tests diff --git a/packages/lint/src/tests/actor-liked-property-required.test.ts b/packages/lint/src/tests/actor-liked-property-required.test.ts index 94c18f7d8..22fad62de 100644 --- a/packages/lint/src/tests/actor-liked-property-required.test.ts +++ b/packages/lint/src/tests/actor-liked-property-required.test.ts @@ -36,10 +36,6 @@ test( `${ruleName}: ✅ Good - dispatcher AFTER (separate) with property`, tests["dispatcher after separate with property"], ); -test( - `${ruleName}: ✅ Good - object literal with property`, - tests["object literal with property"], -); test( `${ruleName}: ❌ Bad - dispatcher configured, property missing`, tests["dispatcher configured property missing"], @@ -52,10 +48,6 @@ test( `${ruleName}: ❌ Bad - dispatcher AFTER (separate), property missing`, tests["dispatcher after separate property missing"], ); -test( - `${ruleName}: ❌ Bad - object literal without property`, - tests["object literal without property"], -); test( `${ruleName}: ❌ Bad - variable assignment without property`, tests["variable assignment without property"], diff --git a/packages/lint/src/tests/actor-outbox-property-mismatch.test.ts b/packages/lint/src/tests/actor-outbox-property-mismatch.test.ts index d047e60c9..d5cc49b4c 100644 --- a/packages/lint/src/tests/actor-outbox-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-outbox-property-mismatch.test.ts @@ -24,15 +24,7 @@ test( `${ruleName}: ✅ Good - property not present`, tests["property not present"], ); -test( - `${ruleName}: ✅ Good - object literal with correct getter`, - tests["object literal with correct getter"], -); test(`${ruleName}: ❌ Bad - wrong getter used`, tests["wrong getter used"]); -test( - `${ruleName}: ❌ Bad - wrong method in object literal`, - tests["wrong method in object literal"], -); test(`${ruleName}: ❌ Bad - wrong identifier`, tests["wrong identifier"]); // Edge case tests diff --git a/packages/lint/src/tests/actor-outbox-property-required.test.ts b/packages/lint/src/tests/actor-outbox-property-required.test.ts index e13828cdd..c55038292 100644 --- a/packages/lint/src/tests/actor-outbox-property-required.test.ts +++ b/packages/lint/src/tests/actor-outbox-property-required.test.ts @@ -36,10 +36,6 @@ test( `${ruleName}: ✅ Good - dispatcher AFTER (separate) with property`, tests["dispatcher after separate with property"], ); -test( - `${ruleName}: ✅ Good - object literal with property`, - tests["object literal with property"], -); test( `${ruleName}: ❌ Bad - dispatcher configured, property missing`, tests["dispatcher configured property missing"], @@ -52,10 +48,6 @@ test( `${ruleName}: ❌ Bad - dispatcher AFTER (separate), property missing`, tests["dispatcher after separate property missing"], ); -test( - `${ruleName}: ❌ Bad - object literal without property`, - tests["object literal without property"], -); test( `${ruleName}: ❌ Bad - variable assignment without property`, tests["variable assignment without property"], diff --git a/packages/lint/src/tests/actor-public-key-required.test.ts b/packages/lint/src/tests/actor-public-key-required.test.ts index 7994243a7..60a25f755 100644 --- a/packages/lint/src/tests/actor-public-key-required.test.ts +++ b/packages/lint/src/tests/actor-public-key-required.test.ts @@ -29,11 +29,11 @@ test(`${ruleName}: ✅ Good - key pairs dispatcher NOT configured, property miss testDenoLint({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return { + return new Person({ id: ctx.getActorUri(identifier), name: "Alice", inbox: ctx.getInboxUri(identifier), - }; + }); }); `, rule, @@ -47,10 +47,10 @@ test(`${ruleName}: ✅ Good - key pairs dispatcher configured BEFORE setActorDis federation .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { const keyPairs = await ctx.getActorKeyPairs(identifier); - return { + return new Person({ id: ctx.getActorUri(identifier), publicKey: keyPairs[0].cryptographicKey, - }; + }); }) .setKeyPairsDispatcher(async (ctx, identifier) => []); `, @@ -66,10 +66,10 @@ test(`${ruleName}: ✅ Good - key pairs dispatcher configured BEFORE setActorDis federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { const keyPairs = await ctx.getActorKeyPairs(identifier); - return { + return new Person({ id: ctx.getActorUri(identifier), publicKey: keyPairs[0].cryptographicKey, - }; + }); }); `, rule, @@ -83,10 +83,10 @@ test(`${ruleName}: ✅ Good - key pairs dispatcher configured AFTER setActorDisp federation .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { const keyPairs = await ctx.getActorKeyPairs(identifier); - return { + return new Person({ id: ctx.getActorUri(identifier), publicKey: keyPairs[0].cryptographicKey, - }; + }); }) .setKeyPairsDispatcher(async (ctx, identifier) => []); `, @@ -100,10 +100,10 @@ test(`${ruleName}: ✅ Good - key pairs dispatcher configured AFTER setActorDisp code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { const keyPairs = await ctx.getActorKeyPairs(identifier); - return { + return new Person({ id: ctx.getActorUri(identifier), publicKey: keyPairs[0].cryptographicKey, - }; + }); }); federation.setKeyPairsDispatcher(async (ctx, identifier) => []); @@ -113,30 +113,15 @@ test(`${ruleName}: ✅ Good - key pairs dispatcher configured AFTER setActorDisp }); }); -test(`${ruleName}: ✅ Good - object literal with \`publicKey\``, () => { - testDenoLint({ - code: ` - federation - .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => ({ - id: ctx.getActorUri(identifier), - publicKey: await ctx.getActorKeyPairs(identifier).then(k => k[0].cryptographicKey), - })) - .setKeyPairsDispatcher(async (ctx, identifier) => []); - `, - rule, - ruleName, - }); -}); - test(`${ruleName}: ❌ Bad - key pairs dispatcher configured BEFORE, property missing`, () => { testDenoLint({ code: ` federation .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return { + return new Person({ id: ctx.getActorUri(identifier), name: "Alice", - }; + }); }) .setKeyPairsDispatcher(async (ctx, identifier) => []); `, @@ -152,10 +137,10 @@ test(`${ruleName}: ❌ Bad - key pairs dispatcher configured BEFORE (separate ca federation.setKeyPairsDispatcher(async (ctx, identifier) => []); federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return { + return new Person({ id: ctx.getActorUri(identifier), name: "Alice", - }; + }); }); `, rule, @@ -168,9 +153,11 @@ test(`${ruleName}: ❌ Bad - key pairs dispatcher configured AFTER, property mis testDenoLint({ code: ` federation - .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => ({ - id: ctx.getActorUri(identifier), - })) + .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + }); + }) .setKeyPairsDispatcher(async (ctx, identifier) => []); `, rule, @@ -182,9 +169,11 @@ test(`${ruleName}: ❌ Bad - key pairs dispatcher configured AFTER, property mis test(`${ruleName}: ❌ Bad - key pairs dispatcher configured AFTER (separate calls), property missing`, () => { testDenoLint({ code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => ({ - id: ctx.getActorUri(identifier), - })); + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + }); + }); federation.setKeyPairsDispatcher(async (ctx, identifier) => []); `, @@ -193,19 +182,3 @@ test(`${ruleName}: ❌ Bad - key pairs dispatcher configured AFTER (separate cal expectedError: actorKeyPropertyRequired("publicKey"), }); }); - -test(`${ruleName}: ❌ Bad - object literal without property`, () => { - testDenoLint({ - code: ` - federation - .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => ({ - id: ctx.getActorUri(identifier), - name: "Alice", - })) - .setKeyPairsDispatcher(async (ctx, identifier) => []); - `, - rule, - ruleName, - expectedError: actorKeyPropertyRequired("publicKey"), - }); -}); diff --git a/packages/lint/src/tests/actor-shared-inbox-property-mismatch.test.ts b/packages/lint/src/tests/actor-shared-inbox-property-mismatch.test.ts index 7c982bf97..058e6425d 100644 --- a/packages/lint/src/tests/actor-shared-inbox-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-shared-inbox-property-mismatch.test.ts @@ -24,15 +24,7 @@ test( `${ruleName}: ✅ Good - property not present`, tests["property not present"], ); -test( - `${ruleName}: ✅ Good - object literal with correct getter`, - tests["object literal with correct getter"], -); test(`${ruleName}: ❌ Bad - wrong getter used`, tests["wrong getter used"]); -test( - `${ruleName}: ❌ Bad - wrong method in object literal`, - tests["wrong method in object literal"], -); test(`${ruleName}: ❌ Bad - wrong identifier`, tests["wrong identifier"]); // Edge case tests diff --git a/packages/lint/src/tests/actor-shared-inbox-property-required.test.ts b/packages/lint/src/tests/actor-shared-inbox-property-required.test.ts index b7334cd4d..254b798d3 100644 --- a/packages/lint/src/tests/actor-shared-inbox-property-required.test.ts +++ b/packages/lint/src/tests/actor-shared-inbox-property-required.test.ts @@ -36,10 +36,6 @@ test( `${ruleName}: ✅ Good - listeners AFTER (separate) with property`, tests["listeners after separate with property"], ); -test( - `${ruleName}: ✅ Good - object literal with property`, - tests["object literal with property"], -); test( `${ruleName}: ❌ Bad - listeners configured, property missing`, tests["listeners configured property missing"], @@ -52,10 +48,6 @@ test( `${ruleName}: ❌ Bad - listeners AFTER (separate), property missing`, tests["listeners after separate property missing"], ); -test( - `${ruleName}: ❌ Bad - object literal without property`, - tests["object literal without property"], -); test( `${ruleName}: ❌ Bad - variable assignment without property`, tests["variable assignment without property"], diff --git a/packages/lint/src/tests/edge-cases.test.ts b/packages/lint/src/tests/edge-cases.test.ts index 168b831e0..137144ca0 100644 --- a/packages/lint/src/tests/edge-cases.test.ts +++ b/packages/lint/src/tests/edge-cases.test.ts @@ -197,20 +197,6 @@ test(`${ruleName}: ❌ Bad - arrow function direct return NewExpression (known l }) )); -test(`${ruleName}: ✅ Good - arrow function direct return with object literal`, () => - actorProperties.forEach((name) => - testDenoLint({ - code: ` - federation.${properties.id.setter}("/users/{identifier}", async (ctx, identifier) => ({ - id: ctx.getActorUri(identifier), - name: "User", - })); - `, - rule, - ruleName, - }) - )); - test( `${ruleName}: ✅ Good - return null (no actor)`, createTestCode(withId(), `if (!identifier) return null;`), From 942c47aab3aabc862f78327feaf2796b5997a55c Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Mon, 8 Dec 2025 05:02:21 +0000 Subject: [PATCH 16/41] Removed object literal lint --- .../lint/src/lib/mismatch-rule-factory.ts | 22 +++---------------- packages/lint/src/lib/property-checker.ts | 12 +++++----- packages/lint/src/tests/edge-cases.test.ts | 3 +-- 3 files changed, 10 insertions(+), 27 deletions(-) diff --git a/packages/lint/src/lib/mismatch-rule-factory.ts b/packages/lint/src/lib/mismatch-rule-factory.ts index 3687894d5..71d8da3f5 100644 --- a/packages/lint/src/lib/mismatch-rule-factory.ts +++ b/packages/lint/src/lib/mismatch-rule-factory.ts @@ -175,17 +175,12 @@ const checkConditionalExpression = return checkConditionalExpression(propertyChecker)(branch); } - // Pattern: new Person({ property: ctx.method() }) + // Pattern: new SomeClass({ property: ctx.method() }) if (isNodeType("NewExpression")(branch)) { return branch.arguments.filter(isNodeType("ObjectExpression")) .some(checkObjectExpression(propertyChecker)); } - // Pattern: { property: ctx.method() } - if (isNodeType("ObjectExpression")(branch)) { - return checkObjectExpression(propertyChecker)(branch); - } - return false; }; @@ -205,17 +200,12 @@ const checkReturnStatement = return checkConditionalExpression(propertyChecker)(arg); } - // Pattern: new Person({ property: ctx.method() }) + // Pattern: new SomeClass({ property: ctx.method() }) if (isNodeType("NewExpression")(arg)) { return arg.arguments.filter(isNodeType("ObjectExpression")) .some(checkObjectExpression(propertyChecker)); } - // Pattern: { property: ctx.method() } - if (isNodeType("ObjectExpression")(arg)) { - return checkObjectExpression(propertyChecker)(arg); - } - return false; }; @@ -234,14 +224,8 @@ const checkFunctionBody = return node.body.some(checkFunctionBody(propertyChecker)); } - // Pattern: arrow function direct return with object literal - // e.g., (ctx, identifier) => ({ id: ctx.getActorUri(identifier) }) - if (isNodeType("ObjectExpression")(node)) { - return checkObjectExpression(propertyChecker)(node); - } - // Pattern: arrow function direct return with new expression - // e.g., (ctx, identifier) => new Person({ id: ctx.getActorUri(identifier) }) + // e.g., (ctx, identifier) => new SomeClass({ id: ctx.getActorUri(identifier) }) if (isNodeType("NewExpression")(node)) { return node.arguments.filter(isNodeType("ObjectExpression")) .some(checkObjectExpression(propertyChecker)); diff --git a/packages/lint/src/lib/property-checker.ts b/packages/lint/src/lib/property-checker.ts index e19673a4a..741be954a 100644 --- a/packages/lint/src/lib/property-checker.ts +++ b/packages/lint/src/lib/property-checker.ts @@ -156,12 +156,11 @@ const extractFirstArgument = (node: ASTNode): ); /** - * Extracts ObjectExpression from NewExpression or direct return. + * Extracts ObjectExpression from NewExpression. */ const extractObjectExpression = ( arg: ASTNode, ): ASTNode & { type: "ObjectExpression" } | null => { - if (isNodeType("ObjectExpression")(arg)) return arg; if (isNodeType("NewExpression")(arg)) return extractFirstArgument(arg); return null; }; @@ -222,7 +221,7 @@ export const createPropertySearcher = (node: unknown): node is | Deno.lint.ReturnStatement | Deno.lint.BlockStatement - | Deno.lint.ObjectExpression => { + | Deno.lint.NewExpression => { if (!isASTNode(node)) return false; if (isNodeType("ReturnStatement")(node)) { @@ -233,9 +232,10 @@ export const createPropertySearcher = return node.body.some(createPropertySearcher(propertyChecker)); } - // Handle arrow function with direct ObjectExpression body: () => ({...}) - if (isNodeType("ObjectExpression")(node)) { - return checkObjectExpression(propertyChecker)(node); + // Handle arrow function with direct NewExpression body: () => new SomeClass({...}) + if (isNodeType("NewExpression")(node)) { + const objExpr = extractFirstArgument(node); + return objExpr ? checkObjectExpression(propertyChecker)(objExpr) : false; } return false; diff --git a/packages/lint/src/tests/edge-cases.test.ts b/packages/lint/src/tests/edge-cases.test.ts index 137144ca0..d049ebb5e 100644 --- a/packages/lint/src/tests/edge-cases.test.ts +++ b/packages/lint/src/tests/edge-cases.test.ts @@ -183,7 +183,7 @@ test( ), ); -test(`${ruleName}: ❌ Bad - arrow function direct return NewExpression (known limitation)`, () => +test(`${ruleName}: ✅ Good - arrow function direct return NewExpression`, () => actorProperties.forEach((name) => testDenoLint({ code: ` @@ -193,7 +193,6 @@ test(`${ruleName}: ❌ Bad - arrow function direct return NewExpression (known l `, rule, ruleName, - expectedError, }) )); From 0e78cfceae4c978a92f60f0defc86de5ab4af10d Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Mon, 8 Dec 2025 05:05:04 +0000 Subject: [PATCH 17/41] Removed object literal lint --- packages/lint/src/tests/edge-cases.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/lint/src/tests/edge-cases.test.ts b/packages/lint/src/tests/edge-cases.test.ts index d049ebb5e..3db493bf5 100644 --- a/packages/lint/src/tests/edge-cases.test.ts +++ b/packages/lint/src/tests/edge-cases.test.ts @@ -188,7 +188,11 @@ test(`${ruleName}: ✅ Good - arrow function direct return NewExpression`, () => testDenoLint({ code: ` federation.${properties.id.setter}("/users/{identifier}", async (ctx, identifier) => - ${withId()} + ${ + withId( + `, ${name}: ctx.${properties[name].getter}(identifier)`, + ) + } ); `, rule, From fa29a2224c790a2be87e93dbbb0fadf5c9411cc1 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Mon, 8 Dec 2025 09:08:50 +0000 Subject: [PATCH 18/41] Refactored rules --- packages/lint/src/lib/const.ts | 48 ++++- packages/lint/src/lib/messages.ts | 51 +++--- .../lint/src/lib/mismatch-rule-factory.ts | 136 ++++++++------ packages/lint/src/lib/property-checker.ts | 39 ++-- .../lint/src/lib/required-rule-factory.ts | 26 +-- packages/lint/src/lib/test-templates.ts | 170 +++++++++++------- packages/lint/src/lib/types.ts | 37 +++- packages/lint/src/mod.ts | 2 +- .../rules/actor-assertion-method-required.ts | 10 +- .../rules/actor-featured-property-mismatch.ts | 7 +- .../rules/actor-featured-property-required.ts | 9 +- .../actor-featured-tags-property-mismatch.ts | 7 +- .../actor-featured-tags-property-required.ts | 9 +- .../actor-followers-property-mismatch.ts | 7 +- .../actor-followers-property-required.ts | 9 +- .../actor-following-property-mismatch.ts | 7 +- .../actor-following-property-required.ts | 9 +- packages/lint/src/rules/actor-id-mismatch.ts | 5 +- packages/lint/src/rules/actor-id-required.ts | 7 +- .../rules/actor-inbox-property-mismatch.ts | 7 +- .../rules/actor-inbox-property-required.ts | 7 +- .../rules/actor-liked-property-mismatch.ts | 7 +- .../rules/actor-liked-property-required.ts | 7 +- .../rules/actor-outbox-property-mismatch.ts | 7 +- .../rules/actor-outbox-property-required.ts | 7 +- .../src/rules/actor-public-key-required.ts | 10 +- .../actor-shared-inbox-property-mismatch.ts | 8 +- .../actor-shared-inbox-property-required.ts | 9 +- 28 files changed, 384 insertions(+), 280 deletions(-) diff --git a/packages/lint/src/lib/const.ts b/packages/lint/src/lib/const.ts index 1b75b9fdf..7feffc362 100644 --- a/packages/lint/src/lib/const.ts +++ b/packages/lint/src/lib/const.ts @@ -1,6 +1,9 @@ +import type { NestedPropertyConfig, PropertyConfig } from "./types.ts"; + export const FEDERATION_SETUP = ` import { createFederation, + Endpoints, MemoryKvStore, InProcessMessageQueue, } from "@fedify/fedify"; @@ -11,6 +14,9 @@ const federation = createFederation({ }); ` as const; +// Re-export types for convenience +export type { NestedPropertyConfig, PropertyConfig }; + /** * Mapping of actor property names to their corresponding Context method names * and dispatcher methods. @@ -19,47 +25,85 @@ const federation = createFederation({ export const properties = { id: { name: "id", + path: ["id"], getter: "getActorUri", setter: "setActorDispatcher", + requiresIdentifier: true, }, following: { name: "following", + path: ["following"], getter: "getFollowingUri", setter: "setFollowingDispatcher", + requiresIdentifier: true, }, followers: { name: "followers", + path: ["followers"], getter: "getFollowersUri", setter: "setFollowersDispatcher", + requiresIdentifier: true, }, outbox: { name: "outbox", + path: ["outbox"], getter: "getOutboxUri", setter: "setOutboxDispatcher", + requiresIdentifier: true, }, inbox: { name: "inbox", + path: ["inbox"], getter: "getInboxUri", setter: "setInboxListeners", + requiresIdentifier: true, }, liked: { name: "liked", + path: ["liked"], getter: "getLikedUri", setter: "setLikedDispatcher", + requiresIdentifier: true, }, featured: { name: "featured", + path: ["featured"], getter: "getFeaturedUri", setter: "setFeaturedDispatcher", + requiresIdentifier: true, }, featuredTags: { name: "featuredTags", + path: ["featuredTags"], getter: "getFeaturedTagsUri", setter: "setFeaturedTagsDispatcher", + requiresIdentifier: true, }, sharedInbox: { - name: "endpoints.sharedInbox", + name: "sharedInbox", + path: ["endpoints", "sharedInbox"], getter: "getInboxUri", setter: "setInboxListeners", + requiresIdentifier: false, + nested: { + parent: "endpoints", + wrapper: "Endpoints", + }, + }, + publicKey: { + name: "publicKey", + path: ["publicKey"], + getter: "getActorKeyPairs", + setter: "setKeyPairsDispatcher", + requiresIdentifier: true, + isKeyProperty: true, + }, + assertionMethod: { + name: "assertionMethod", + path: ["assertionMethod"], + getter: "getActorKeyPairs", + setter: "setKeyPairsDispatcher", + requiresIdentifier: true, + isKeyProperty: true, }, -} as const; +} as const satisfies Record; diff --git a/packages/lint/src/lib/messages.ts b/packages/lint/src/lib/messages.ts index 144e8ee5a..1759794e4 100644 --- a/packages/lint/src/lib/messages.ts +++ b/packages/lint/src/lib/messages.ts @@ -1,26 +1,28 @@ -import { getArticle } from "./utils.ts"; +import type { MethodCallContext, PropertyConfig } from "./types.ts"; /** * Generates error message for *-required rules. * Used when a property is missing from the actor dispatcher return value. * - * @param propertyName - The property name (e.g., "id", "inbox", "following") + * @param name - The property name (e.g., "id", "inbox", "following") */ -export const actorPropertyRequired = (propertyName: string): string => - `Actor dispatcher must return an actor with ${ - getArticle(propertyName) - } \`${propertyName}\` property.`; - -/** - * Generates error message for publicKey and assertionMethod required rules. - * These use getActorKeyPairs instead of a property-specific getter. - * - * @param propertyName - The property name (e.g., "publicKey", "assertionMethod") - */ -export const actorKeyPropertyRequired = (propertyName: string): string => - `${ - actorPropertyRequired(propertyName) - } Use \`Context.getActorKeyPairs(identifier)\` to retrieve key pairs.`; +export const actorPropertyRequired = ({ + setter, + path, + getter, + requiresIdentifier = true, +}: PropertyConfig): string => + `When \`${setter}\` is configured, the \`${ + path.join(".") + }\` property is recommended. Use \`${ + getExpectedCall({ + ctxName: "Context", + methodName: getter, + idName: "identifier", + path: path.join("."), + requiresIdentifier, + }) + }\` to retrieve key pairs.`; /** * Generates error message for *-mismatch rules. @@ -30,10 +32,19 @@ export const actorKeyPropertyRequired = (propertyName: string): string => * @param expectedCall - The expected method call (e.g., "ctx.getActorUri(identifier)") */ export const actorPropertyMismatch = ( - propertyName: string, - expectedCall: string, + context: MethodCallContext, +): string => + `Actor's \`${context.path}\` property must match \`${ + getExpectedCall(context) + }\`. \ +Ensure you're using the correct context method.`; + +const getExpectedCall = ( + { ctxName, methodName, requiresIdentifier, idName }: MethodCallContext, ): string => - `Actor's \`${propertyName}\` property must match \`${expectedCall}\`. Ensure you're using the correct context method.`; + requiresIdentifier + ? `${ctxName}.${methodName}(${idName})` + : `${ctxName}.${methodName}()`; /** * Error message for collection filtering not implemented. diff --git a/packages/lint/src/lib/mismatch-rule-factory.ts b/packages/lint/src/lib/mismatch-rule-factory.ts index 71d8da3f5..3f800fa6e 100644 --- a/packages/lint/src/lib/mismatch-rule-factory.ts +++ b/packages/lint/src/lib/mismatch-rule-factory.ts @@ -26,7 +26,7 @@ import type { ASTNode, FunctionNode, MethodCallContext, - MismatchRuleConfig, + PropertyConfig, } from "./types.ts"; import { eq } from "./utils.ts"; @@ -96,24 +96,43 @@ const isPropertyWithKeyName = (path: string) => * Creates a property existence checker for the given property path. * Only checks if the property exists, not its value. */ -const createPropertyExistenceChecker = (propertyPath: string) => { - const path = propertyPath.split("."); - const checkPropertyExists = (path: string[]) => (node: ASTNode): boolean => { - if (!isPropertyWithKeyName(path[0])(node)) return false; - - // Base case: last property in path - if (path.length === 1) return true; - - // Nested case: check the nested object - const value = node.value; - if (!isNodeType("ObjectExpression")(value)) return false; - - return pipe( - value.properties, - filterASTNodes, - some(checkPropertyExists(path.slice(1))), - ); - }; +const createPropertyExistenceChecker = (path: readonly string[]) => { + const checkPropertyExists = + (path: readonly string[]) => (node: ASTNode): boolean => { + if (!isPropertyWithKeyName(path[0])(node)) return false; + + // Base case: last property in path + if (path.length === 1) return true; + + // Nested case: check the nested object or NewExpression + const value = node.value; + + // Handle ObjectExpression: endpoints: { sharedInbox: ... } + if (isNodeType("ObjectExpression")(value)) { + return pipe( + value.properties, + filterASTNodes, + some(checkPropertyExists(path.slice(1))), + ); + } + + // Handle NewExpression: endpoints: new Endpoints({ sharedInbox: ... }) + if (isNodeType("NewExpression")(value)) { + const args = value.arguments; + if (!isArray(args) || args.length === 0) return false; + const firstArg = args[0]; + if (!isASTNode(firstArg) || !isNodeType("ObjectExpression")(firstArg)) { + return false; + } + return pipe( + firstArg.properties, + filterASTNodes, + some(checkPropertyExists(path.slice(1))), + ); + } + + return false; + }; return (prop: ASTNode): boolean => allOf(isASTNode, checkPropertyExists(path))(prop); @@ -121,33 +140,49 @@ const createPropertyExistenceChecker = (propertyPath: string) => { /** * Creates a property value checker for the given property path. - * Handles both simple properties (e.g., "id") and nested properties (e.g., ["endpoints", "sharedInbox"]). + * Handles both simple properties (e.g., ["id"]) and nested properties (e.g., ["endpoints", "sharedInbox"]). */ const createPropertyValueChecker = ( - propertyPath: string, + path: readonly string[], ctx: MethodCallContext, ) => { - const path = propertyPath.split("."); - const checkPropertyValue = (path: string[]) => (prop: ASTNode): boolean => { - if (!isPropertyWithKeyName(path[0])(prop)) return false; - - // Base case: last property in path - const value = prop.value; - if (path.length === 1) { - return isExpectedMethodCall(value, ctx); - } + const checkPropertyValue = + (path: readonly string[]) => (prop: ASTNode): boolean => { + if (!isPropertyWithKeyName(path[0])(prop)) return false; + + // Base case: last property in path + const value = prop.value; + if (path.length === 1) { + return isExpectedMethodCall(value, ctx); + } - // Nested case: check the nested object - if (!isNodeType("ObjectExpression")(value)) { - return false; - } + // Nested case: check the nested object or NewExpression + // Handle ObjectExpression: endpoints: { sharedInbox: ... } + if (isNodeType("ObjectExpression")(value)) { + return pipe( + value.properties, + filterASTNodes, + some(checkPropertyValue(path.slice(1))), + ); + } - return pipe( - value.properties, - filterASTNodes, - some(checkPropertyValue(path.slice(1))), - ); - }; + // Handle NewExpression: endpoints: new Endpoints({ sharedInbox: ... }) + if (isNodeType("NewExpression")(value)) { + const args = value.arguments; + if (!isArray(args) || args.length === 0) return false; + const firstArg = args[0]; + if (!isASTNode(firstArg) || !isNodeType("ObjectExpression")(firstArg)) { + return false; + } + return pipe( + firstArg.properties, + filterASTNodes, + some(checkPropertyValue(path.slice(1))), + ); + } + + return false; + }; return (prop: ASTNode): boolean => allOf(isASTNode, checkPropertyValue(path))(prop); @@ -259,15 +294,11 @@ const getNameIfIdentifier = (node: Deno.lint.Parameter): string | null => /** * Creates a lint rule that checks if a property uses the correct context method. * - * @param config Configuration object containing property path, method name, and identifier requirement + * @param config Property configuration containing name, getter, setter, and nested info * @returns A Deno lint rule */ export const createMismatchRule = ( - { - propertyPath, - methodName, - requiresIdentifier = true, - }: MismatchRuleConfig, + { path, getter, requiresIdentifier = true }: PropertyConfig, ): Deno.lint.Rule => { return { create(context) { @@ -290,14 +321,15 @@ export const createMismatchRule = ( if (!ctxName || !idName) return; const methodCallContext: MethodCallContext = { + path: path.join("."), ctxName, idName, - methodName, + methodName: getter, requiresIdentifier, }; // Check if the property exists first - const existenceChecker = createPropertyExistenceChecker(propertyPath); + const existenceChecker = createPropertyExistenceChecker(path); const hasProperty = checkFunctionBody(existenceChecker)( dispatcherArg.body, ); @@ -307,7 +339,7 @@ export const createMismatchRule = ( // Property exists, now check if the value is correct const valueChecker = createPropertyValueChecker( - propertyPath, + path, methodCallContext, ); const hasCorrectValue = checkFunctionBody(valueChecker)( @@ -315,13 +347,9 @@ export const createMismatchRule = ( ); if (!hasCorrectValue) { - const expectedCall = requiresIdentifier - ? `${ctxName}.${methodName}(${idName})` - : `${ctxName}.${methodName}()`; - context.report({ node: dispatcherArg, - message: actorPropertyMismatch(propertyPath, expectedCall), + message: actorPropertyMismatch(methodCallContext), }); } }, diff --git a/packages/lint/src/lib/property-checker.ts b/packages/lint/src/lib/property-checker.ts index 741be954a..a2582e91c 100644 --- a/packages/lint/src/lib/property-checker.ts +++ b/packages/lint/src/lib/property-checker.ts @@ -88,7 +88,7 @@ const isProperty = (node: ASTNode): node is Deno.lint.Property => * This avoids circular dependency between hasNestedProperty and createNestedPropertyChecker. */ const checkNestedPropertyPath = - (path: string[]) => (node: unknown): boolean => { + (path: readonly string[]) => (node: unknown): boolean => { if (!isASTNode(node) || !hasKeyName(path[0])(node)) return false; // Base case: single property @@ -98,16 +98,35 @@ const checkNestedPropertyPath = // Recursive case: check nested properties if (!isProperty(node)) return false; - if (!hasObjectExpressionValue(node)) return false; - const properties = node.value.properties; + const value = node.value; - // Check if any property matches the remaining path - return pipe( - properties, - filter(isASTNode), - some(checkNestedPropertyPath(path.slice(1))), - ); + // Handle ObjectExpression: endpoints: { sharedInbox: ... } + if (hasObjectExpressionValue(node)) { + const properties = node.value.properties; + return pipe( + properties, + filter(isASTNode), + some(checkNestedPropertyPath(path.slice(1))), + ); + } + + // Handle NewExpression: endpoints: new Endpoints({ sharedInbox: ... }) + if (isNodeType("NewExpression")(value)) { + const args = value.arguments; + if (!Array.isArray(args) || args.length === 0) return false; + const firstArg = args[0]; + if (!isASTNode(firstArg) || !isNodeType("ObjectExpression")(firstArg)) { + return false; + } + return pipe( + firstArg.properties, + filter(isASTNode), + some(checkNestedPropertyPath(path.slice(1))), + ); + } + + return false; }; /** @@ -116,7 +135,7 @@ const checkNestedPropertyPath = * @returns A predicate function that checks if the nested property exists */ export const createNestedPropertyChecker = - (path: string[]) => (node: unknown): boolean => + (path: readonly string[]) => (node: unknown): boolean => checkNestedPropertyPath(path)(node); /** diff --git a/packages/lint/src/lib/required-rule-factory.ts b/packages/lint/src/lib/required-rule-factory.ts index ee0fcb467..316786298 100644 --- a/packages/lint/src/lib/required-rule-factory.ts +++ b/packages/lint/src/lib/required-rule-factory.ts @@ -1,3 +1,4 @@ +import { actorPropertyRequired } from "./messages.ts"; import { allOf, hasIdentifierProperty, @@ -16,6 +17,7 @@ import type { CallMemberExpression, CallMemberExpressionWithIdentified, FunctionNode, + PropertyConfig, } from "./types.ts"; /** @@ -62,31 +64,21 @@ interface ActorDispatcherInfo { } /** - * Internal configuration for the unified rule factory. - */ -interface InternalRequiredConfig { - propertyName: string; - dispatcherMethod: string; - errorMessage: string; -} - -/** - * Creates a required rule with the given configuration. + * Creates a required rule with the given property configuration. */ export function createRequiredRule( - config: InternalRequiredConfig, + config: PropertyConfig, ): Deno.lint.Rule { - const propertyPath = config.propertyName.split("."); - const propertyChecker = propertyPath.length === 1 - ? createPropertyChecker(propertyPath[0]) - : createNestedPropertyChecker(propertyPath); + const propertyChecker = config.path.length === 1 + ? createPropertyChecker(config.path[0]) + : createNestedPropertyChecker(config.path); const propertySearcher = createPropertySearcher(propertyChecker); return { create(context) { const federationTracker = trackFederationVariables(); const dispatcherTracker = createDispatcherTracker( - config.dispatcherMethod, + config.setter, federationTracker, ); const actorDispatchers: ActorDispatcherInfo[] = []; @@ -113,7 +105,7 @@ export function createRequiredRule( if (!propertySearcher(dispatcherArg.body)) { context.report({ node: dispatcherArg, - message: config.errorMessage, + message: actorPropertyRequired(config), }); } } diff --git a/packages/lint/src/lib/test-templates.ts b/packages/lint/src/lib/test-templates.ts index 2553af811..8f57b24ab 100644 --- a/packages/lint/src/lib/test-templates.ts +++ b/packages/lint/src/lib/test-templates.ts @@ -1,14 +1,7 @@ -import { properties } from "./const.ts"; -import { - actorKeyPropertyRequired, - actorPropertyMismatch, - actorPropertyRequired, -} from "./messages.ts"; +import { properties, type PropertyConfig } from "./const.ts"; +import { actorPropertyMismatch, actorPropertyRequired } from "./messages.ts"; import { testDenoLint } from "./test.ts"; -// deno-lint-ignore no-explicit-any -type Rule = any; - // ============================================================================= // Types // ============================================================================= @@ -52,21 +45,83 @@ const createSeparateDispatcherCode = ( return isBefore ? `${dispatcher}\n${actor}` : `${actor}\n${dispatcher}`; }; +// ============================================================================= +// Property Code Generation Utilities +// ============================================================================= + +/** + * Generates the method call code for a property. + * @param getter The getter method name + * @param requiresIdentifier Whether the method requires an identifier parameter + * @param ctxName The context variable name (default: "ctx") + * @param idName The identifier variable name (default: "identifier") + */ +const createMethodCall = ( + getter: string, + requiresIdentifier: boolean, + ctxName = "ctx", + idName = "identifier", +): string => { + return requiresIdentifier + ? `${ctxName}.${getter}(${idName})` + : `${ctxName}.${getter}()`; +}; + +/** + * Generates property assignment code for a given property configuration. + * Handles both simple properties and nested properties with wrappers. + * + * @example + * // Simple property: id: ctx.getActorUri(identifier), + * // Nested property: endpoints: new Endpoints({ sharedInbox: ctx.getInboxUri() }), + */ +const createPropertyAssignment = ( + prop: PropertyConfig, + options: { + getter?: string; + ctxName?: string; + idName?: string; + } = {}, +): string => { + const getter = options.getter ?? prop.getter; + const ctxName = options.ctxName ?? "ctx"; + const idName = options.idName ?? "identifier"; + const requiresIdentifier = prop.requiresIdentifier ?? true; + + const methodCall = createMethodCall( + getter, + requiresIdentifier, + ctxName, + idName, + ); + + if (prop.nested) { + return `${prop.nested.parent}: new ${prop.nested.wrapper}({ ${prop.name}: ${methodCall} }),`; + } + return `${prop.name}: ${methodCall},`; +}; + +/** + * Generates the expected method call string for error messages. + */ +const getExpectedMethodCall = ( + prop: PropertyConfig, + ctxName = "ctx", + idName = "identifier", +): string => { + const requiresIdentifier = prop.requiresIdentifier ?? true; + return createMethodCall(prop.getter, requiresIdentifier, ctxName, idName); +}; + const createTestCode = ( propertyKey: PropertyKey, includeProperty: boolean, ) => { const prop = properties[propertyKey]; - const isNested = prop.name.includes("."); let propertyCode = ""; if (includeProperty) { - if (isNested) { - // endpoints.sharedInbox - propertyCode = `endpoints: { sharedInbox: ctx.${prop.getter}() },`; - } else { - propertyCode = `${prop.name}: ctx.${prop.getter}(identifier),`; - } + propertyCode = createPropertyAssignment(prop); } return `return new Person({ @@ -234,16 +289,14 @@ export function createRequiredListenerRuleTests( const { rule, ruleName } = config; const prop = properties[propertyKey]; const expectedError = actorPropertyRequired(prop.name); - const isNested = propertyKey === "sharedInbox"; - const createPropertyCode = (include: boolean) => { + const createLocalPropertyCode = (include: boolean) => { if (!include) return ""; - if (isNested) return "endpoints: { sharedInbox: ctx.getInboxUri() },"; - return "inbox: ctx.getInboxUri(identifier),"; + return createPropertyAssignment(prop); }; const createActorCode = (includeProperty: boolean) => { - const propCode = createPropertyCode(includeProperty); + const propCode = createLocalPropertyCode(includeProperty); return `return new Person({ id: ctx.getActorUri(identifier), ${propCode} @@ -497,7 +550,7 @@ export function createKeyRequiredRuleTests( config: TestConfig, ) { const { rule, ruleName } = config; - const expectedError = actorKeyPropertyRequired(propertyName); + const expectedError = actorPropertyRequired(propertyName); const createActorWithKey = (includeProperty: boolean) => { const propCode = includeProperty @@ -651,13 +704,13 @@ export function createMismatchRuleTests( ) { const { rule, ruleName } = config; const prop = properties[propertyKey]; - const isNested = prop.name.includes("."); // Build the expected method call string - const expectedMethodCall = isNested - ? `ctx.${prop.getter}()` - : `ctx.${prop.getter}(identifier)`; - const expectedError = actorPropertyMismatch(prop.name, expectedMethodCall); + const expectedMethodCall = getExpectedMethodCall(prop); + const expectedError = actorPropertyMismatch( + prop.path.join("."), + expectedMethodCall, + ); // Find a wrong getter for testing const wrongGetters = Object.values(properties) @@ -665,18 +718,13 @@ export function createMismatchRuleTests( .map((p) => p.getter); const wrongGetter = wrongGetters[0] || "getWrongUri"; - const createPropertyCode = (getter: string) => { - if (isNested) { - // endpoints.sharedInbox - return `endpoints: { sharedInbox: ctx.${getter}() },`; - } - return `${prop.name}: ctx.${getter}(identifier),`; - }; + const createLocalPropertyCode = (getter: string) => + createPropertyAssignment(prop, { getter }); const createActorCode = (getter: string) => `return new Person({ id: ctx.getActorUri(identifier), - ${createPropertyCode(getter)} + ${createLocalPropertyCode(getter)} name: "John Doe", });`; @@ -714,9 +762,9 @@ export function createMismatchRuleTests( // ❌ Bad - wrong identifier "wrong identifier": () => { - const propCode = isNested - ? `endpoints: { sharedInbox: wrongContext.${prop.getter}() },` - : `${prop.name}: wrongContext.${prop.getter}(identifier),`; + const propCode = createPropertyAssignment(prop, { + ctxName: "wrongContext", + }); testDenoLint({ code: createDispatcherCode(`return new Person({ id: ctx.getActorUri(identifier), @@ -854,17 +902,13 @@ export function createRequiredEdgeCaseTests( const { rule, ruleName } = config; const prop = properties[propertyKey]; const expectedError = actorPropertyRequired(prop.name); - const isNested = prop.name.includes("."); - const createPropertyCode = () => { - if (isNested) return "endpoints: { sharedInbox: ctx.getInboxUri() },"; - return `${prop.name}: ctx.${prop.getter}(identifier),`; - }; + const createLocalPropertyCode = () => createPropertyAssignment(prop); return { // ✅ Ternary with property in both branches "ternary with property in both branches": () => { - const propCode = createPropertyCode(); + const propCode = createLocalPropertyCode(); testDenoLint({ code: createChainedDispatcherCode( `return condition @@ -879,7 +923,7 @@ export function createRequiredEdgeCaseTests( // ❌ Ternary missing property in consequent "ternary missing property in consequent": () => { - const propCode = createPropertyCode(); + const propCode = createLocalPropertyCode(); testDenoLint({ code: createChainedDispatcherCode( `return condition @@ -895,7 +939,7 @@ export function createRequiredEdgeCaseTests( // ❌ Ternary missing property in alternate "ternary missing property in alternate": () => { - const propCode = createPropertyCode(); + const propCode = createLocalPropertyCode(); testDenoLint({ code: createChainedDispatcherCode( `return condition @@ -926,7 +970,7 @@ export function createRequiredEdgeCaseTests( // ✅ Nested ternary with property "nested ternary with property": () => { - const propCode = createPropertyCode(); + const propCode = createLocalPropertyCode(); testDenoLint({ code: createChainedDispatcherCode( `return condition1 @@ -952,13 +996,13 @@ export function createMismatchEdgeCaseTests( ) { const { rule, ruleName } = config; const prop = properties[propertyKey]; - const isNested = prop.name.includes("."); // Build the expected method call string - const expectedMethodCall = isNested - ? `ctx.${prop.getter}()` - : `ctx.${prop.getter}(identifier)`; - const expectedError = actorPropertyMismatch(prop.name, expectedMethodCall); + const expectedMethodCall = getExpectedMethodCall(prop); + const expectedError = actorPropertyMismatch( + prop.path.join("."), + expectedMethodCall, + ); // Find a wrong getter for testing const wrongGetters = Object.values(properties) @@ -966,15 +1010,13 @@ export function createMismatchEdgeCaseTests( .map((p) => p.getter); const wrongGetter = wrongGetters[0] || "getWrongUri"; - const createPropertyCode = (getter: string) => { - if (isNested) return `endpoints: { sharedInbox: ctx.${getter}() },`; - return `${prop.name}: ctx.${getter}(identifier),`; - }; + const createLocalPropertyCode = (getter: string) => + createPropertyAssignment(prop, { getter }); return { // ✅ Ternary with correct getter in both branches "ternary with correct getter in both branches": () => { - const propCode = createPropertyCode(prop.getter); + const propCode = createLocalPropertyCode(prop.getter); testDenoLint({ code: createDispatcherCode( `return condition @@ -988,8 +1030,8 @@ export function createMismatchEdgeCaseTests( // ❌ Ternary with wrong getter in consequent "ternary with wrong getter in consequent": () => { - const correctPropCode = createPropertyCode(prop.getter); - const wrongPropCode = createPropertyCode(wrongGetter); + const correctPropCode = createLocalPropertyCode(prop.getter); + const wrongPropCode = createLocalPropertyCode(wrongGetter); testDenoLint({ code: createDispatcherCode( `return condition @@ -1004,8 +1046,8 @@ export function createMismatchEdgeCaseTests( // ❌ Ternary with wrong getter in alternate "ternary with wrong getter in alternate": () => { - const correctPropCode = createPropertyCode(prop.getter); - const wrongPropCode = createPropertyCode(wrongGetter); + const correctPropCode = createLocalPropertyCode(prop.getter); + const wrongPropCode = createLocalPropertyCode(wrongGetter); testDenoLint({ code: createDispatcherCode( `return condition @@ -1020,7 +1062,7 @@ export function createMismatchEdgeCaseTests( // ❌ Ternary with wrong getter in both "ternary with wrong getter in both branches": () => { - const wrongPropCode = createPropertyCode(wrongGetter); + const wrongPropCode = createLocalPropertyCode(wrongGetter); testDenoLint({ code: createDispatcherCode( `return condition @@ -1035,7 +1077,7 @@ export function createMismatchEdgeCaseTests( // ✅ Nested ternary with correct getter "nested ternary with correct getter": () => { - const propCode = createPropertyCode(prop.getter); + const propCode = createLocalPropertyCode(prop.getter); testDenoLint({ code: createDispatcherCode( `return condition1 @@ -1222,7 +1264,7 @@ export function createKeyRequiredEdgeCaseTests( config: TestConfig, ) { const { rule, ruleName } = config; - const expectedError = actorKeyPropertyRequired(propertyName); + const expectedError = actorPropertyRequired(propertyName); const createPropertyCode = () => `${propertyName}: ctx.getActorKeyPairs(identifier),`; diff --git a/packages/lint/src/lib/types.ts b/packages/lint/src/lib/types.ts index d002d7b6c..0eddbeaa2 100644 --- a/packages/lint/src/lib/types.ts +++ b/packages/lint/src/lib/types.ts @@ -22,16 +22,34 @@ export type FunctionNode = | Deno.lint.FunctionExpression; /** - * Configuration for property mismatch rules. - * These rules check if a property uses the correct `ctx.get*()` method. + * Configuration for nested property wrappers. + * Used when a property needs to be wrapped in a class instance (e.g., `new Endpoints({...})`). */ -export interface MismatchRuleConfig { - /** Property path to check. Can be a single property name or an array for nested properties. */ - propertyPath: string; - /** Expected context method name (e.g., "getActorUri", "getInboxUri") */ - methodName: string; - /** Whether the method requires identifier parameter (default: true) */ - requiresIdentifier?: boolean; +export interface NestedPropertyConfig { + /** Parent property name (e.g., "endpoints") */ + parent: string; + /** Wrapper class name (e.g., "Endpoints") */ + wrapper: string; +} + +/** + * Configuration for an actor property. + */ +export interface PropertyConfig { + /** Property name (e.g., "id", "sharedInbox") */ + name: string; + /** Full property path for lint rules (e.g., ["id"], ["endpoints", "sharedInbox"]) */ + path: readonly string[]; + /** Context method name to get the URI (e.g., "getActorUri", "getInboxUri") */ + getter: string; + /** Dispatcher/Listener method name (e.g., "setActorDispatcher", "setInboxListeners") */ + setter: string; + /** Whether the getter requires an identifier parameter (default: true) */ + requiresIdentifier: boolean; + /** Nested property configuration, if this property is nested inside another */ + nested?: NestedPropertyConfig; + /** Whether this is a key-related property (uses getActorKeyPairs) */ + isKeyProperty?: boolean; } export type ASTNode = @@ -42,6 +60,7 @@ export type ASTNode = * Context for method call validation. */ export interface MethodCallContext { + path: string; ctxName: string; idName: string; methodName: string; diff --git a/packages/lint/src/mod.ts b/packages/lint/src/mod.ts index ecd7508ab..7d26ef4f7 100644 --- a/packages/lint/src/mod.ts +++ b/packages/lint/src/mod.ts @@ -63,7 +63,7 @@ import actorSharedInboxPropertyRequired, { } from "./rules/collection-filtering-not-implemented.ts"; */ const plugin: Deno.lint.Plugin = { - name: "@fedify/lint", + name: "fedify-lint", rules: { [ACTOR_ID_MISMATCH]: actorIdMismatch, [ACTOR_ID_REQUIRED]: actorIdRequired, diff --git a/packages/lint/src/rules/actor-assertion-method-required.ts b/packages/lint/src/rules/actor-assertion-method-required.ts index a9a7bbe62..716f3efef 100644 --- a/packages/lint/src/rules/actor-assertion-method-required.ts +++ b/packages/lint/src/rules/actor-assertion-method-required.ts @@ -1,13 +1,11 @@ -import { actorKeyPropertyRequired } from "../lib/messages.ts"; +import { properties } from "../lib/const.ts"; import { createRequiredRule } from "../lib/required-rule-factory.ts"; export const ACTOR_ASSERTION_METHOD_REQUIRED = "actor-assertion-method-required"; -const actorAssertionMethodRequired: Deno.lint.Rule = createRequiredRule({ - propertyName: "assertionMethod", - dispatcherMethod: "setKeyPairsDispatcher", - errorMessage: actorKeyPropertyRequired("assertionMethod"), -}); +const actorAssertionMethodRequired: Deno.lint.Rule = createRequiredRule( + properties.assertionMethod, +); export default actorAssertionMethodRequired; diff --git a/packages/lint/src/rules/actor-featured-property-mismatch.ts b/packages/lint/src/rules/actor-featured-property-mismatch.ts index 651a4d904..425215cf4 100644 --- a/packages/lint/src/rules/actor-featured-property-mismatch.ts +++ b/packages/lint/src/rules/actor-featured-property-mismatch.ts @@ -4,9 +4,8 @@ import { createMismatchRule } from "../lib/mismatch-rule-factory.ts"; export const ACTOR_FEATURED_PROPERTY_MISMATCH = "actor-featured-property-mismatch" as const; -const actorFeaturedPropertyMismatch: Deno.lint.Rule = createMismatchRule({ - propertyPath: properties.featured.name, - methodName: properties.featured.getter, -}); +const actorFeaturedPropertyMismatch: Deno.lint.Rule = createMismatchRule( + properties.featured, +); export default actorFeaturedPropertyMismatch; diff --git a/packages/lint/src/rules/actor-featured-property-required.ts b/packages/lint/src/rules/actor-featured-property-required.ts index 30e686b49..77902d7e2 100644 --- a/packages/lint/src/rules/actor-featured-property-required.ts +++ b/packages/lint/src/rules/actor-featured-property-required.ts @@ -1,14 +1,11 @@ import { properties } from "../lib/const.ts"; -import { actorPropertyRequired } from "../lib/messages.ts"; import { createRequiredRule } from "../lib/required-rule-factory.ts"; export const ACTOR_FEATURED_PROPERTY_REQUIRED = "actor-featured-property-required"; -const actorFeaturedPropertyRequired = createRequiredRule({ - propertyName: properties.featured.name, - dispatcherMethod: properties.featured.setter, - errorMessage: actorPropertyRequired(properties.featured.name), -}); +const actorFeaturedPropertyRequired = createRequiredRule( + properties.featured, +); export default actorFeaturedPropertyRequired; diff --git a/packages/lint/src/rules/actor-featured-tags-property-mismatch.ts b/packages/lint/src/rules/actor-featured-tags-property-mismatch.ts index 26b5afa6f..e1a53d113 100644 --- a/packages/lint/src/rules/actor-featured-tags-property-mismatch.ts +++ b/packages/lint/src/rules/actor-featured-tags-property-mismatch.ts @@ -4,9 +4,8 @@ import { createMismatchRule } from "../lib/mismatch-rule-factory.ts"; export const ACTOR_FEATURED_TAGS_PROPERTY_MISMATCH = "actor-featured-tags-property-mismatch" as const; -const actorFeaturedTagsPropertyMismatch: Deno.lint.Rule = createMismatchRule({ - propertyPath: properties.featuredTags.name, - methodName: properties.featuredTags.getter, -}); +const actorFeaturedTagsPropertyMismatch: Deno.lint.Rule = createMismatchRule( + properties.featuredTags, +); export default actorFeaturedTagsPropertyMismatch; diff --git a/packages/lint/src/rules/actor-featured-tags-property-required.ts b/packages/lint/src/rules/actor-featured-tags-property-required.ts index 33c40db63..c34e07190 100644 --- a/packages/lint/src/rules/actor-featured-tags-property-required.ts +++ b/packages/lint/src/rules/actor-featured-tags-property-required.ts @@ -1,14 +1,11 @@ import { properties } from "../lib/const.ts"; -import { actorPropertyRequired } from "../lib/messages.ts"; import { createRequiredRule } from "../lib/required-rule-factory.ts"; export const ACTOR_FEATURED_TAGS_PROPERTY_REQUIRED = "actor-featured-tags-property-required"; -const actorFeaturedTagsPropertyRequired = createRequiredRule({ - propertyName: properties.featuredTags.name, - dispatcherMethod: properties.featuredTags.setter, - errorMessage: actorPropertyRequired(properties.featuredTags.name), -}); +const actorFeaturedTagsPropertyRequired = createRequiredRule( + properties.featuredTags, +); export default actorFeaturedTagsPropertyRequired; diff --git a/packages/lint/src/rules/actor-followers-property-mismatch.ts b/packages/lint/src/rules/actor-followers-property-mismatch.ts index 2f78bebaf..473b233a3 100644 --- a/packages/lint/src/rules/actor-followers-property-mismatch.ts +++ b/packages/lint/src/rules/actor-followers-property-mismatch.ts @@ -4,9 +4,8 @@ import { createMismatchRule } from "../lib/mismatch-rule-factory.ts"; export const ACTOR_FOLLOWERS_PROPERTY_MISMATCH = "actor-followers-property-mismatch" as const; -const actorFollowersPropertyMismatch: Deno.lint.Rule = createMismatchRule({ - propertyPath: properties.followers.name, - methodName: properties.followers.getter, -}); +const actorFollowersPropertyMismatch: Deno.lint.Rule = createMismatchRule( + properties.followers, +); export default actorFollowersPropertyMismatch; diff --git a/packages/lint/src/rules/actor-followers-property-required.ts b/packages/lint/src/rules/actor-followers-property-required.ts index 92c24aa73..7dace83ae 100644 --- a/packages/lint/src/rules/actor-followers-property-required.ts +++ b/packages/lint/src/rules/actor-followers-property-required.ts @@ -1,14 +1,11 @@ import { properties } from "../lib/const.ts"; -import { actorPropertyRequired } from "../lib/messages.ts"; import { createRequiredRule } from "../lib/required-rule-factory.ts"; export const ACTOR_FOLLOWERS_PROPERTY_REQUIRED = "actor-followers-property-required"; -const actorFollowersPropertyRequired = createRequiredRule({ - propertyName: properties.followers.name, - dispatcherMethod: properties.followers.setter, - errorMessage: actorPropertyRequired(properties.followers.name), -}); +const actorFollowersPropertyRequired = createRequiredRule( + properties.followers, +); export default actorFollowersPropertyRequired; diff --git a/packages/lint/src/rules/actor-following-property-mismatch.ts b/packages/lint/src/rules/actor-following-property-mismatch.ts index 671290228..ed7497d0b 100644 --- a/packages/lint/src/rules/actor-following-property-mismatch.ts +++ b/packages/lint/src/rules/actor-following-property-mismatch.ts @@ -4,9 +4,8 @@ import { createMismatchRule } from "../lib/mismatch-rule-factory.ts"; export const ACTOR_FOLLOWING_PROPERTY_MISMATCH = "actor-following-property-mismatch" as const; -const actorFollowingPropertyMismatch: Deno.lint.Rule = createMismatchRule({ - propertyPath: properties.following.name, - methodName: properties.following.getter, -}); +const actorFollowingPropertyMismatch: Deno.lint.Rule = createMismatchRule( + properties.following, +); export default actorFollowingPropertyMismatch; diff --git a/packages/lint/src/rules/actor-following-property-required.ts b/packages/lint/src/rules/actor-following-property-required.ts index fd01522ac..7863b7f9d 100644 --- a/packages/lint/src/rules/actor-following-property-required.ts +++ b/packages/lint/src/rules/actor-following-property-required.ts @@ -1,14 +1,11 @@ import { properties } from "../lib/const.ts"; -import { actorPropertyRequired } from "../lib/messages.ts"; import { createRequiredRule } from "../lib/required-rule-factory.ts"; export const ACTOR_FOLLOWING_PROPERTY_REQUIRED = "actor-following-property-required"; -const actorFollowingPropertyRequired = createRequiredRule({ - propertyName: properties.following.name, - dispatcherMethod: properties.following.setter, - errorMessage: actorPropertyRequired(properties.following.name), -}); +const actorFollowingPropertyRequired = createRequiredRule( + properties.following, +); export default actorFollowingPropertyRequired; diff --git a/packages/lint/src/rules/actor-id-mismatch.ts b/packages/lint/src/rules/actor-id-mismatch.ts index 5ea6a5d7e..bb8ddea0c 100644 --- a/packages/lint/src/rules/actor-id-mismatch.ts +++ b/packages/lint/src/rules/actor-id-mismatch.ts @@ -10,9 +10,6 @@ export const ACTOR_ID_MISMATCH = "actor-id-mismatch" as const; * This is a `*-mismatch` rule that validates the VALUE of the property, * not just its existence. For checking property existence, use `actor-id-required`. */ -const actorIdMismatch = createMismatchRule({ - propertyPath: properties.id.name, - methodName: properties.id.getter, -}); +const actorIdMismatch = createMismatchRule(properties.id); export default actorIdMismatch; diff --git a/packages/lint/src/rules/actor-id-required.ts b/packages/lint/src/rules/actor-id-required.ts index 15713612b..8cf21d2e3 100644 --- a/packages/lint/src/rules/actor-id-required.ts +++ b/packages/lint/src/rules/actor-id-required.ts @@ -1,5 +1,4 @@ import { properties } from "../lib/const.ts"; -import { actorPropertyRequired } from "../lib/messages.ts"; import { createRequiredRule } from "../lib/required-rule-factory.ts"; export const ACTOR_ID_REQUIRED = "actor-id-required" as const; @@ -11,10 +10,6 @@ export const ACTOR_ID_REQUIRED = "actor-id-required" as const; * For checking if the value uses the correct `ctx.getActorUri()` method, * use `actor-id-mismatch`. */ -const actorIdRequired = createRequiredRule({ - propertyName: properties.id.name, - dispatcherMethod: properties.id.setter, - errorMessage: actorPropertyRequired(properties.id.name), -}); +const actorIdRequired = createRequiredRule(properties.id); export default actorIdRequired; diff --git a/packages/lint/src/rules/actor-inbox-property-mismatch.ts b/packages/lint/src/rules/actor-inbox-property-mismatch.ts index 5ac4d480f..88960f66c 100644 --- a/packages/lint/src/rules/actor-inbox-property-mismatch.ts +++ b/packages/lint/src/rules/actor-inbox-property-mismatch.ts @@ -4,9 +4,8 @@ import { createMismatchRule } from "../lib/mismatch-rule-factory.ts"; export const ACTOR_INBOX_PROPERTY_MISMATCH = "actor-inbox-property-mismatch" as const; -const actorInboxPropertyMismatch: Deno.lint.Rule = createMismatchRule({ - propertyPath: properties.inbox.name, - methodName: properties.inbox.getter, -}); +const actorInboxPropertyMismatch: Deno.lint.Rule = createMismatchRule( + properties.inbox, +); export default actorInboxPropertyMismatch; diff --git a/packages/lint/src/rules/actor-inbox-property-required.ts b/packages/lint/src/rules/actor-inbox-property-required.ts index 25f37f960..5f066d5cf 100644 --- a/packages/lint/src/rules/actor-inbox-property-required.ts +++ b/packages/lint/src/rules/actor-inbox-property-required.ts @@ -1,13 +1,8 @@ import { properties } from "../lib/const.ts"; -import { actorPropertyRequired } from "../lib/messages.ts"; import { createRequiredRule } from "../lib/required-rule-factory.ts"; export const ACTOR_INBOX_PROPERTY_REQUIRED = "actor-inbox-property-required"; -const actorInboxPropertyRequired = createRequiredRule({ - propertyName: properties.inbox.name, - dispatcherMethod: properties.inbox.setter, - errorMessage: actorPropertyRequired(properties.inbox.name), -}); +const actorInboxPropertyRequired = createRequiredRule(properties.inbox); export default actorInboxPropertyRequired; diff --git a/packages/lint/src/rules/actor-liked-property-mismatch.ts b/packages/lint/src/rules/actor-liked-property-mismatch.ts index 1a95df34e..5777331df 100644 --- a/packages/lint/src/rules/actor-liked-property-mismatch.ts +++ b/packages/lint/src/rules/actor-liked-property-mismatch.ts @@ -4,9 +4,8 @@ import { createMismatchRule } from "../lib/mismatch-rule-factory.ts"; export const ACTOR_LIKED_PROPERTY_MISMATCH = "actor-liked-property-mismatch" as const; -const actorLikedPropertyMismatch: Deno.lint.Rule = createMismatchRule({ - propertyPath: properties.liked.name, - methodName: properties.liked.getter, -}); +const actorLikedPropertyMismatch: Deno.lint.Rule = createMismatchRule( + properties.liked, +); export default actorLikedPropertyMismatch; diff --git a/packages/lint/src/rules/actor-liked-property-required.ts b/packages/lint/src/rules/actor-liked-property-required.ts index e020af631..9a2890bf2 100644 --- a/packages/lint/src/rules/actor-liked-property-required.ts +++ b/packages/lint/src/rules/actor-liked-property-required.ts @@ -1,13 +1,8 @@ import { properties } from "../lib/const.ts"; -import { actorPropertyRequired } from "../lib/messages.ts"; import { createRequiredRule } from "../lib/required-rule-factory.ts"; export const ACTOR_LIKED_PROPERTY_REQUIRED = "actor-liked-property-required"; -const actorLikedPropertyRequired = createRequiredRule({ - propertyName: properties.liked.name, - dispatcherMethod: properties.liked.setter, - errorMessage: actorPropertyRequired(properties.liked.name), -}); +const actorLikedPropertyRequired = createRequiredRule(properties.liked); export default actorLikedPropertyRequired; diff --git a/packages/lint/src/rules/actor-outbox-property-mismatch.ts b/packages/lint/src/rules/actor-outbox-property-mismatch.ts index e00df1de7..4589d5443 100644 --- a/packages/lint/src/rules/actor-outbox-property-mismatch.ts +++ b/packages/lint/src/rules/actor-outbox-property-mismatch.ts @@ -4,9 +4,8 @@ import { createMismatchRule } from "../lib/mismatch-rule-factory.ts"; export const ACTOR_OUTBOX_PROPERTY_MISMATCH = "actor-outbox-property-mismatch" as const; -const actorOutboxPropertyMismatch: Deno.lint.Rule = createMismatchRule({ - propertyPath: properties.outbox.name, - methodName: properties.outbox.getter, -}); +const actorOutboxPropertyMismatch: Deno.lint.Rule = createMismatchRule( + properties.outbox, +); export default actorOutboxPropertyMismatch; diff --git a/packages/lint/src/rules/actor-outbox-property-required.ts b/packages/lint/src/rules/actor-outbox-property-required.ts index a5822ae96..c4f5c3082 100644 --- a/packages/lint/src/rules/actor-outbox-property-required.ts +++ b/packages/lint/src/rules/actor-outbox-property-required.ts @@ -1,13 +1,8 @@ import { properties } from "../lib/const.ts"; -import { actorPropertyRequired } from "../lib/messages.ts"; import { createRequiredRule } from "../lib/required-rule-factory.ts"; export const ACTOR_OUTBOX_PROPERTY_REQUIRED = "actor-outbox-property-required"; -const actorOutboxPropertyRequired = createRequiredRule({ - propertyName: properties.outbox.name, - dispatcherMethod: properties.outbox.setter, - errorMessage: actorPropertyRequired(properties.outbox.name), -}); +const actorOutboxPropertyRequired = createRequiredRule(properties.outbox); export default actorOutboxPropertyRequired; diff --git a/packages/lint/src/rules/actor-public-key-required.ts b/packages/lint/src/rules/actor-public-key-required.ts index 45f4ed0b6..65d826f5e 100644 --- a/packages/lint/src/rules/actor-public-key-required.ts +++ b/packages/lint/src/rules/actor-public-key-required.ts @@ -1,12 +1,10 @@ -import { actorKeyPropertyRequired } from "../lib/messages.ts"; +import { properties } from "../lib/const.ts"; import { createRequiredRule } from "../lib/required-rule-factory.ts"; export const ACTOR_PUBLIC_KEY_REQUIRED = "actor-public-key-required"; -const actorPublicKeyRequired: Deno.lint.Rule = createRequiredRule({ - propertyName: "publicKey", - dispatcherMethod: "setKeyPairsDispatcher", - errorMessage: actorKeyPropertyRequired("publicKey"), -}); +const actorPublicKeyRequired: Deno.lint.Rule = createRequiredRule( + properties.publicKey, +); export default actorPublicKeyRequired; diff --git a/packages/lint/src/rules/actor-shared-inbox-property-mismatch.ts b/packages/lint/src/rules/actor-shared-inbox-property-mismatch.ts index 52c419de5..4093652eb 100644 --- a/packages/lint/src/rules/actor-shared-inbox-property-mismatch.ts +++ b/packages/lint/src/rules/actor-shared-inbox-property-mismatch.ts @@ -4,10 +4,8 @@ import { createMismatchRule } from "../lib/mismatch-rule-factory.ts"; export const ACTOR_SHARED_INBOX_PROPERTY_MISMATCH = "actor-shared-inbox-property-mismatch" as const; -const actorSharedInboxPropertyMismatch: Deno.lint.Rule = createMismatchRule({ - propertyPath: properties.sharedInbox.name, - methodName: properties.sharedInbox.getter, - requiresIdentifier: false, -}); +const actorSharedInboxPropertyMismatch: Deno.lint.Rule = createMismatchRule( + properties.sharedInbox, +); export default actorSharedInboxPropertyMismatch; diff --git a/packages/lint/src/rules/actor-shared-inbox-property-required.ts b/packages/lint/src/rules/actor-shared-inbox-property-required.ts index 4956c58e1..ccd1295c6 100644 --- a/packages/lint/src/rules/actor-shared-inbox-property-required.ts +++ b/packages/lint/src/rules/actor-shared-inbox-property-required.ts @@ -1,14 +1,11 @@ import { properties } from "../lib/const.ts"; -import { actorPropertyRequired } from "../lib/messages.ts"; import { createRequiredRule } from "../lib/required-rule-factory.ts"; export const ACTOR_SHARED_INBOX_PROPERTY_REQUIRED = "actor-shared-inbox-property-required"; -const actorSharedInboxPropertyRequired = createRequiredRule({ - propertyName: properties.sharedInbox.name, - dispatcherMethod: properties.sharedInbox.setter, - errorMessage: actorPropertyRequired(properties.sharedInbox.name), -}); +const actorSharedInboxPropertyRequired = createRequiredRule( + properties.sharedInbox, +); export default actorSharedInboxPropertyRequired; From 5f7ed13456141ce5838556d104f4df2ab85ac0c6 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Mon, 8 Dec 2025 09:56:18 +0000 Subject: [PATCH 19/41] Completed `collection-filtering-not-implemented` and fixed other rules --- packages/lint/src/lib/test-templates.ts | 56 +++-- packages/lint/src/mod.ts | 6 +- .../collection-filtering-not-implemented.ts | 38 ++- .../actor-assertion-method-required.test.ts | 11 +- .../lint/src/tests/actor-id-mismatch.test.ts | 19 +- .../lint/src/tests/actor-id-required.test.ts | 216 +++++++++++++++++- .../tests/actor-public-key-required.test.ts | 11 +- ...llection-filtering-not-implemented.test.ts | 196 ++++++++++------ packages/lint/src/tests/edge-cases.test.ts | 206 ----------------- 9 files changed, 415 insertions(+), 344 deletions(-) delete mode 100644 packages/lint/src/tests/edge-cases.test.ts diff --git a/packages/lint/src/lib/test-templates.ts b/packages/lint/src/lib/test-templates.ts index 8f57b24ab..1bc1059e9 100644 --- a/packages/lint/src/lib/test-templates.ts +++ b/packages/lint/src/lib/test-templates.ts @@ -1,6 +1,7 @@ import { properties, type PropertyConfig } from "./const.ts"; import { actorPropertyMismatch, actorPropertyRequired } from "./messages.ts"; import { testDenoLint } from "./test.ts"; +import type { MethodCallContext } from "./types.ts"; // ============================================================================= // Types @@ -9,7 +10,7 @@ import { testDenoLint } from "./test.ts"; type PropertyKey = keyof typeof properties; interface TestConfig { - rule: Rule; + rule: Deno.lint.Rule; ruleName: string; } @@ -102,16 +103,19 @@ const createPropertyAssignment = ( }; /** - * Generates the expected method call string for error messages. + * Creates a MethodCallContext for error message generation. */ -const getExpectedMethodCall = ( +const createMethodCallContext = ( prop: PropertyConfig, ctxName = "ctx", idName = "identifier", -): string => { - const requiresIdentifier = prop.requiresIdentifier ?? true; - return createMethodCall(prop.getter, requiresIdentifier, ctxName, idName); -}; +): MethodCallContext => ({ + path: prop.path.join("."), + ctxName, + idName, + methodName: prop.getter, + requiresIdentifier: prop.requiresIdentifier ?? true, +}); const createTestCode = ( propertyKey: PropertyKey, @@ -145,7 +149,7 @@ export function createRequiredDispatcherRuleTests( ) { const { rule, ruleName } = config; const prop = properties[propertyKey]; - const expectedError = actorPropertyRequired(prop.name); + const expectedError = actorPropertyRequired(prop); return { // ✅ Good - non-Federation object @@ -288,7 +292,7 @@ export function createRequiredListenerRuleTests( ) { const { rule, ruleName } = config; const prop = properties[propertyKey]; - const expectedError = actorPropertyRequired(prop.name); + const expectedError = actorPropertyRequired(prop); const createLocalPropertyCode = (include: boolean) => { if (!include) return ""; @@ -441,7 +445,7 @@ export function createRequiredListenerRuleTests( */ export function createIdRequiredRuleTests(config: TestConfig) { const { rule, ruleName } = config; - const expectedError = actorPropertyRequired(properties.id.name); + const expectedError = actorPropertyRequired(properties.id); return { // ✅ Good - non-Federation object @@ -550,7 +554,8 @@ export function createKeyRequiredRuleTests( config: TestConfig, ) { const { rule, ruleName } = config; - const expectedError = actorPropertyRequired(propertyName); + const prop = properties[propertyName]; + const expectedError = actorPropertyRequired(prop); const createActorWithKey = (includeProperty: boolean) => { const propCode = includeProperty @@ -705,12 +710,8 @@ export function createMismatchRuleTests( const { rule, ruleName } = config; const prop = properties[propertyKey]; - // Build the expected method call string - const expectedMethodCall = getExpectedMethodCall(prop); - const expectedError = actorPropertyMismatch( - prop.path.join("."), - expectedMethodCall, - ); + // Build the expected method call context + const expectedError = actorPropertyMismatch(createMethodCallContext(prop)); // Find a wrong getter for testing const wrongGetters = Object.values(properties) @@ -797,8 +798,7 @@ export function createMismatchRuleTests( export function createIdMismatchRuleTests(config: TestConfig) { const { rule, ruleName } = config; const expectedError = actorPropertyMismatch( - properties.id.name, - properties.id.getter, + createMethodCallContext(properties.id), ); return { @@ -901,7 +901,7 @@ export function createRequiredEdgeCaseTests( ) { const { rule, ruleName } = config; const prop = properties[propertyKey]; - const expectedError = actorPropertyRequired(prop.name); + const expectedError = actorPropertyRequired(prop); const createLocalPropertyCode = () => createPropertyAssignment(prop); @@ -997,12 +997,8 @@ export function createMismatchEdgeCaseTests( const { rule, ruleName } = config; const prop = properties[propertyKey]; - // Build the expected method call string - const expectedMethodCall = getExpectedMethodCall(prop); - const expectedError = actorPropertyMismatch( - prop.path.join("."), - expectedMethodCall, - ); + // Build the expected method call context + const expectedError = actorPropertyMismatch(createMethodCallContext(prop)); // Find a wrong getter for testing const wrongGetters = Object.values(properties) @@ -1098,7 +1094,7 @@ export function createMismatchEdgeCaseTests( */ export function createIdRequiredEdgeCaseTests(config: TestConfig) { const { rule, ruleName } = config; - const expectedError = actorPropertyRequired(properties.id.name); + const expectedError = actorPropertyRequired(properties.id); return { // ✅ Ternary with id in both branches @@ -1179,8 +1175,7 @@ export function createIdRequiredEdgeCaseTests(config: TestConfig) { export function createIdMismatchEdgeCaseTests(config: TestConfig) { const { rule, ruleName } = config; const expectedError = actorPropertyMismatch( - properties.id.name, - properties.id.getter, + createMethodCallContext(properties.id), ); return { @@ -1264,7 +1259,8 @@ export function createKeyRequiredEdgeCaseTests( config: TestConfig, ) { const { rule, ruleName } = config; - const expectedError = actorPropertyRequired(propertyName); + const prop = properties[propertyName]; + const expectedError = actorPropertyRequired(prop); const createPropertyCode = () => `${propertyName}: ctx.getActorKeyPairs(identifier),`; diff --git a/packages/lint/src/mod.ts b/packages/lint/src/mod.ts index 7d26ef4f7..64f5f6c76 100644 --- a/packages/lint/src/mod.ts +++ b/packages/lint/src/mod.ts @@ -58,9 +58,9 @@ import actorSharedInboxPropertyMismatch, { import actorSharedInboxPropertyRequired, { ACTOR_SHARED_INBOX_PROPERTY_REQUIRED, } from "./rules/actor-shared-inbox-property-required.ts"; -/* import collectionFilteringNotImplemented, { +import collectionFilteringNotImplemented, { COLLECTION_FILTERING_NOT_IMPLEMENTED, -} from "./rules/collection-filtering-not-implemented.ts"; */ +} from "./rules/collection-filtering-not-implemented.ts"; const plugin: Deno.lint.Plugin = { name: "fedify-lint", @@ -85,7 +85,7 @@ const plugin: Deno.lint.Plugin = { [ACTOR_SHARED_INBOX_PROPERTY_MISMATCH]: actorSharedInboxPropertyMismatch, [ACTOR_PUBLIC_KEY_REQUIRED]: actorPublicKeyRequired, [ACTOR_ASSERTION_METHOD_REQUIRED]: actorAssertionMethodRequired, - // [COLLECTION_FILTERING_NOT_IMPLEMENTED]: collectionFilteringNotImplemented, + [COLLECTION_FILTERING_NOT_IMPLEMENTED]: collectionFilteringNotImplemented, }, }; diff --git a/packages/lint/src/rules/collection-filtering-not-implemented.ts b/packages/lint/src/rules/collection-filtering-not-implemented.ts index 6e53bc903..31dd8ba28 100644 --- a/packages/lint/src/rules/collection-filtering-not-implemented.ts +++ b/packages/lint/src/rules/collection-filtering-not-implemented.ts @@ -19,30 +19,24 @@ export const COLLECTION_FILTERING_NOT_IMPLEMENTED = "collection-filtering-not-implemented"; /** - * Collection dispatcher methods that support filtering. - * These are the setXxxDispatcher methods for collections. + * The followers dispatcher method that supports filtering. + * Only setFollowersDispatcher uses the filter parameter for + * followers collection synchronization. + * See: https://fedify.dev/manual/collections#filtering-by-server */ -const COLLECTION_DISPATCHER_METHODS = [ - "setFollowersDispatcher", - "setFollowingDispatcher", - "setOutboxDispatcher", - "setLikedDispatcher", - "setFeaturedDispatcher", - "setFeaturedTagsDispatcher", -] as const; +const FOLLOWERS_DISPATCHER_METHOD = "setFollowersDispatcher" as const; /** - * Checks if a node is a collection dispatcher call. + * Checks if a node is a setFollowersDispatcher call. */ -const isCollectionDispatcherCall = ( +const isFollowersDispatcherCall = ( node: Deno.lint.CallExpression, ): node is Deno.lint.CallExpression & CallMemberExpressionWithIdentified => allOf( hasMemberExpressionCallee, hasIdentifierProperty, hasMinArguments(2), - (n: Deno.lint.CallExpression & CallMemberExpressionWithIdentified) => - COLLECTION_DISPATCHER_METHODS.some((method) => hasMethodName(method)(n)), + hasMethodName(FOLLOWERS_DISPATCHER_METHOD), )(node as Deno.lint.CallExpression & CallMemberExpressionWithIdentified); /** @@ -57,16 +51,20 @@ const hasFilterParameter = (fn: FunctionNode): boolean => { /** * Lint rule: collection-filtering-not-implemented * - * Warns when a collection dispatcher doesn't implement filtering. - * Collection dispatchers should accept a 4th parameter (filter) to support - * server-side filtering and avoid large response payloads. + * Warns when setFollowersDispatcher doesn't implement filtering. + * The followers dispatcher should accept a 4th parameter (filter/baseUri) to support + * server-side filtering for followers collection synchronization. + * See: https://fedify.dev/manual/collections#filtering-by-server * * @example Good: * ```ts * federation.setFollowersDispatcher( * "/users/{identifier}/followers", * async (ctx, identifier, cursor, filter) => { - * // Implementation with filter support + * // filter is a URL representing the base URI to filter by + * if (filter != null) { + * // Filter followers by server + * } * return { items: [] }; * } * ); @@ -91,8 +89,8 @@ const collectionFilteringNotImplementedRule: Deno.lint.Rule = { VariableDeclarator: federationTracker.VariableDeclarator, CallExpression(node) { - // Check if it's a collection dispatcher call on a federation object - if (!isCollectionDispatcherCall(node)) return; + // Check if it's a setFollowersDispatcher call on a federation object + if (!isFollowersDispatcherCall(node)) return; if (!federationTracker.isFederationObject(node.callee.object)) return; // Get the dispatcher callback (2nd argument) diff --git a/packages/lint/src/tests/actor-assertion-method-required.test.ts b/packages/lint/src/tests/actor-assertion-method-required.test.ts index d270f6758..3d13ad642 100644 --- a/packages/lint/src/tests/actor-assertion-method-required.test.ts +++ b/packages/lint/src/tests/actor-assertion-method-required.test.ts @@ -1,5 +1,6 @@ import { test } from "node:test"; -import { actorKeyPropertyRequired } from "../lib/messages.ts"; +import { properties } from "../lib/const.ts"; +import { actorPropertyRequired } from "../lib/messages.ts"; import { testDenoLint } from "../lib/test.ts"; import { ACTOR_ASSERTION_METHOD_REQUIRED as ruleName, @@ -132,7 +133,7 @@ test(`${ruleName}: ❌ Bad - key pairs dispatcher configured BEFORE, property mi `, rule, ruleName, - expectedError: actorKeyPropertyRequired("assertionMethod"), + expectedError: actorPropertyRequired(properties.assertionMethod), }); }); @@ -150,7 +151,7 @@ test(`${ruleName}: ❌ Bad - key pairs dispatcher configured BEFORE (chained), p `, rule, ruleName, - expectedError: actorKeyPropertyRequired("assertionMethod"), + expectedError: actorPropertyRequired(properties.assertionMethod), }); }); @@ -170,7 +171,7 @@ test(`${ruleName}: ❌ Bad - key pairs dispatcher configured AFTER, property mis `, rule, ruleName, - expectedError: actorKeyPropertyRequired("assertionMethod"), + expectedError: actorPropertyRequired(properties.assertionMethod), }); }); @@ -188,6 +189,6 @@ test(`${ruleName}: ❌ Bad - key pairs dispatcher configured AFTER (chained), pr `, rule, ruleName, - expectedError: actorKeyPropertyRequired("assertionMethod"), + expectedError: actorPropertyRequired(properties.assertionMethod), }); }); diff --git a/packages/lint/src/tests/actor-id-mismatch.test.ts b/packages/lint/src/tests/actor-id-mismatch.test.ts index 18257f00a..133626428 100644 --- a/packages/lint/src/tests/actor-id-mismatch.test.ts +++ b/packages/lint/src/tests/actor-id-mismatch.test.ts @@ -1,4 +1,5 @@ import { test } from "node:test"; +import { properties } from "../lib/const.ts"; import { actorPropertyMismatch } from "../lib/messages.ts"; import { testDenoLint } from "../lib/test.ts"; import { @@ -6,6 +7,14 @@ import { default as rule, } from "../rules/actor-id-mismatch.ts"; +const expectedError = actorPropertyMismatch({ + path: properties.id.path.join("."), + ctxName: "ctx", + idName: "identifier", + methodName: properties.id.getter, + requiresIdentifier: properties.id.requiresIdentifier, +}); + test(`${ruleName}: ✅ Good - \`setActorDispatcher\` called on non-Federation object`, () => { testDenoLint({ code: ` @@ -69,7 +78,7 @@ test(`${ruleName}: ❌ Bad - id uses hardcoded string instead of ctx.getActorUri `, rule, ruleName, - expectedError: actorPropertyMismatch("id", "ctx.getActorUri(identifier)"), + expectedError: expectedError, }); }); @@ -85,7 +94,7 @@ test(`${ruleName}: ❌ Bad - id uses wrong method (getInboxUri instead of getAct `, rule, ruleName, - expectedError: actorPropertyMismatch("id", "ctx.getActorUri(identifier)"), + expectedError: expectedError, }); }); @@ -101,7 +110,7 @@ test(`${ruleName}: ❌ Bad - id uses wrong identifier parameter`, () => { `, rule, ruleName, - expectedError: actorPropertyMismatch("id", "ctx.getActorUri(identifier)"), + expectedError: expectedError, }); }); @@ -160,7 +169,7 @@ test(`${ruleName} Edge: ❌ spread operator with wrong id after spread`, () => { `, rule, ruleName, - expectedError: actorPropertyMismatch("id", "ctx.getActorUri(identifier)"), + expectedError: expectedError, }); }); @@ -191,6 +200,6 @@ test(`${ruleName} Edge: ❌ arrow function direct return with wrong id`, () => { `, rule, ruleName, - expectedError: actorPropertyMismatch("id", "ctx.getActorUri(identifier)"), + expectedError: expectedError, }); }); diff --git a/packages/lint/src/tests/actor-id-required.test.ts b/packages/lint/src/tests/actor-id-required.test.ts index 7af9fd260..311b5fe0c 100644 --- a/packages/lint/src/tests/actor-id-required.test.ts +++ b/packages/lint/src/tests/actor-id-required.test.ts @@ -83,7 +83,7 @@ test(`${ruleName}: ❌ Bad - without \`id\` property`, () => { `, rule, ruleName, - expectedError: actorPropertyRequired(properties.id.name), + expectedError: actorPropertyRequired(properties.id), }); }); @@ -96,7 +96,7 @@ test(`${ruleName}: ❌ Bad - returning empty object`, () => { `, rule, ruleName, - expectedError: actorPropertyRequired(properties.id.name), + expectedError: actorPropertyRequired(properties.id), }); }); @@ -129,6 +129,216 @@ test(`${ruleName}: ❌ Bad - variable assignment without \`id\``, () => { `, rule, ruleName, - expectedError: actorPropertyRequired(properties.id.name), + expectedError: actorPropertyRequired(properties.id), + }); +}); + +// ============================================================================= +// Edge Cases +// ============================================================================= + +const withId = (extra = "") => + `new Person({ id: ctx.getActorUri(identifier), name: "User"${extra} })`; +const withoutId = () => `new Person({ name: "User" })`; + +test(`${ruleName}: ✅ Edge - multiple return statements - all have id`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + if (identifier === "admin") { + return ${withId()}; + } + return identifier === "admin" + ? ${withId()} + : ${withId()}; + }); + `, + rule, + ruleName, + }); +}); + +test(`${ruleName}: ✅ Edge - multiple return statements - first missing id (known limitation)`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + if (identifier === "admin") { + return ${withoutId()}; + } + return ${withId()}; + }); + `, + rule, + ruleName, + }); +}); + +test(`${ruleName}: ❌ Edge - multiple return statements - second missing id`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + if (identifier === "admin") { + return ${withId()}; + } + return ${withoutId()}; + }); + `, + rule, + ruleName, + expectedError: actorPropertyRequired(properties.id), + }); +}); + +test(`${ruleName}: ✅ Edge - if/else with else block (known limitation: else not checked)`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + if (identifier) { + return ${withId()}; + } else { + return ${withoutId()}; + } + return ${withId()}; + }); + `, + rule, + ruleName, + }); +}); + +test(`${ruleName}: ✅ Edge - nested if with id`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + if (identifier) { + if (identifier === "admin") { + return ${withId()}; + } + return ${withId()}; + } + return ${withId()}; + }); + `, + rule, + ruleName, + }); +}); + +test(`${ruleName}: ✅ Edge - ternary operator with id in both branches`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return identifier ? ${withId()} : ${withId()}; + }); + `, + rule, + ruleName, + }); +}); + +test(`${ruleName}: ❌ Edge - ternary operator without id in consequent`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return identifier ? ${withoutId()} : ${withId()}; + }); + `, + rule, + ruleName, + expectedError: actorPropertyRequired(properties.id), + }); +}); + +test(`${ruleName}: ❌ Edge - ternary operator without id in alternate`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return identifier ? ${withId()} : ${withoutId()}; + }); + `, + rule, + ruleName, + expectedError: actorPropertyRequired(properties.id), + }); +}); + +test(`${ruleName}: ✅ Edge - spread operator with id property after spread`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + const base = { name: "User" }; + return new Person({ ...base, id: ctx.getActorUri(identifier) }); + }); + `, + rule, + ruleName, + }); +}); + +test(`${ruleName}: ❌ Edge - spread operator with id in spread source (known limitation)`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + const base = { id: ctx.getActorUri(identifier), name: "User" }; + return new Person({ ...base }); + }); + `, + rule, + ruleName, + expectedError: actorPropertyRequired(properties.id), + }); +}); + +test(`${ruleName}: ❌ Edge - variable assignment then return (known limitation)`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + const actor = ${withId()}; + return actor; + }); + `, + rule, + ruleName, + expectedError: actorPropertyRequired(properties.id), + }); +}); + +test(`${ruleName}: ❌ Edge - property assignment after construction (known limitation)`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + const actor = ${withoutId()}; + actor.id = ctx.getActorUri(identifier); + return actor; + }); + `, + rule, + ruleName, + expectedError: actorPropertyRequired(properties.id), + }); +}); + +test(`${ruleName}: ✅ Edge - arrow function direct return NewExpression`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => + ${withId()} + ); + `, + rule, + ruleName, + }); +}); + +test(`${ruleName}: ✅ Edge - return null (no actor)`, () => { + testDenoLint({ + code: ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + if (!identifier) return null; + return ${withId()}; + }); + `, + rule, + ruleName, }); }); diff --git a/packages/lint/src/tests/actor-public-key-required.test.ts b/packages/lint/src/tests/actor-public-key-required.test.ts index 60a25f755..ce5dc7eba 100644 --- a/packages/lint/src/tests/actor-public-key-required.test.ts +++ b/packages/lint/src/tests/actor-public-key-required.test.ts @@ -1,5 +1,6 @@ import { test } from "node:test"; -import { actorKeyPropertyRequired } from "../lib/messages.ts"; +import { properties } from "../lib/const.ts"; +import { actorPropertyRequired } from "../lib/messages.ts"; import { testDenoLint } from "../lib/test.ts"; import { ACTOR_PUBLIC_KEY_REQUIRED as ruleName, @@ -127,7 +128,7 @@ test(`${ruleName}: ❌ Bad - key pairs dispatcher configured BEFORE, property mi `, rule, ruleName, - expectedError: actorKeyPropertyRequired("publicKey"), + expectedError: actorPropertyRequired(properties.publicKey), }); }); @@ -145,7 +146,7 @@ test(`${ruleName}: ❌ Bad - key pairs dispatcher configured BEFORE (separate ca `, rule, ruleName, - expectedError: actorKeyPropertyRequired("publicKey"), + expectedError: actorPropertyRequired(properties.publicKey), }); }); @@ -162,7 +163,7 @@ test(`${ruleName}: ❌ Bad - key pairs dispatcher configured AFTER, property mis `, rule, ruleName, - expectedError: actorKeyPropertyRequired("publicKey"), + expectedError: actorPropertyRequired(properties.publicKey), }); }); @@ -179,6 +180,6 @@ test(`${ruleName}: ❌ Bad - key pairs dispatcher configured AFTER (separate cal `, rule, ruleName, - expectedError: actorKeyPropertyRequired("publicKey"), + expectedError: actorPropertyRequired(properties.publicKey), }); }); diff --git a/packages/lint/src/tests/collection-filtering-not-implemented.test.ts b/packages/lint/src/tests/collection-filtering-not-implemented.test.ts index 6270c239b..341b9131e 100644 --- a/packages/lint/src/tests/collection-filtering-not-implemented.test.ts +++ b/packages/lint/src/tests/collection-filtering-not-implemented.test.ts @@ -9,90 +9,157 @@ import { const filterless = ["ctx", "identifier", "cursor"] as const; +// Helper to create test code for setFollowersDispatcher +const createFollowersDispatcherCode = ( + { + params = ["ctx", "identifier", "cursor", "filter"], + async = true, + arrow = true, + }: { + params?: readonly string[]; + async?: boolean; + arrow?: boolean; + } = {}, +): string => { + const paramsString = params.join(", "); + const asyncKeyword = async ? "async" : ""; + const [funcKeyword, arrowSymbol] = arrow ? ["", "=>"] : ["function", ""]; + + return ` + federation.setFollowersDispatcher( + "/users/{identifier}/followers", + ${asyncKeyword} ${funcKeyword}(${paramsString}) ${arrowSymbol} { + return { items: [] }; + } + ); + `; +}; + test( `${ruleName}: ✅ Good - async arrow function with filter parameter`, - createTestCode(), + () => + testDenoLint({ + code: createFollowersDispatcherCode(), + rule, + ruleName, + }), ); test( `${ruleName}: ✅ Good - async function expression with filter`, - createTestCode({ arrow: false }), + () => + testDenoLint({ + code: createFollowersDispatcherCode({ arrow: false }), + rule, + ruleName, + }), ); test( `${ruleName}: ✅ Good - sync arrow function with filter`, - createTestCode({ async: false }), + () => + testDenoLint({ + code: createFollowersDispatcherCode({ async: false }), + rule, + ruleName, + }), ); test( `${ruleName}: ✅ Good - sync function expression with filter`, - createTestCode({ async: false, arrow: false }), + () => + testDenoLint({ + code: createFollowersDispatcherCode({ async: false, arrow: false }), + rule, + ruleName, + }), ); test( `${ruleName}: ❌ Bad - async arrow function without filter parameter`, - createTestCode({ params: filterless }, expectedError), + () => + testDenoLint({ + code: createFollowersDispatcherCode({ params: filterless }), + rule, + ruleName, + expectedError, + }), ); test( `${ruleName}: ❌ Bad - async function expression without filter`, - createTestCode({ params: filterless, arrow: false }, expectedError), + () => + testDenoLint({ + code: createFollowersDispatcherCode({ params: filterless, arrow: false }), + rule, + ruleName, + expectedError, + }), ); test( `${ruleName}: ❌ Bad - sync arrow function expression without filter`, - createTestCode({ params: filterless, async: false }, expectedError), + () => + testDenoLint({ + code: createFollowersDispatcherCode({ params: filterless, async: false }), + rule, + ruleName, + expectedError, + }), ); test( `${ruleName}: ❌ Bad - sync function expression without filter`, - createTestCode( - { params: filterless, async: false, arrow: false }, - expectedError, - ), + () => + testDenoLint({ + code: createFollowersDispatcherCode({ + params: filterless, + async: false, + arrow: false, + }), + rule, + ruleName, + expectedError, + }), ); test( `${ruleName}: ✅ Good - 4th parameter but unnamed filter`, - createTestCode({ params: ["ctx", "identifier", "cursor", "somethingElse"] }), + () => + testDenoLint({ + code: createFollowersDispatcherCode({ + params: ["ctx", "identifier", "cursor", "baseUri"], + }), + rule, + ruleName, + }), ); test( `${ruleName}: ❌ Bad - only two parameters (missing cursor and filter)`, - createTestCode({ params: ["ctx", "identifier"] }, expectedError), -); - -test(`${ruleName}: ✅ Good - non-federation object is not checked`, () => - filterNeeded.forEach((name) => + () => testDenoLint({ - code: createDispatcherCode(name, { params: filterless }), + code: createFollowersDispatcherCode({ params: ["ctx", "identifier"] }), rule, ruleName, - federationSetup: ` - const federation = { - ${properties[name].setter}: () => {} - }; - `, - }) - )); - -function createTestCode( - codeOptions: Parameters[1] = {}, - expectedError?: string, -) { - return () => - filterNeeded.forEach((name) => - testDenoLint({ - code: createDispatcherCode(name, codeOptions), - rule, - ruleName, - expectedError, - }) - ); -} + expectedError, + }), +); -const filterNeeded = [ - "followers", +test(`${ruleName}: ✅ Good - non-federation object is not checked`, () => + testDenoLint({ + code: createFollowersDispatcherCode({ params: filterless }), + rule, + ruleName, + federationSetup: ` + const federation = { + setFollowersDispatcher: () => {} + }; + `, + })); + +// Test that other collection dispatchers are NOT checked +const otherDispatchers = [ "following", "outbox", "liked", @@ -100,28 +167,23 @@ const filterNeeded = [ "featuredTags", ] as const satisfies (keyof typeof properties)[]; -const createDispatcherCode = ( - name: keyof typeof properties, - { - params = ["ctx", "identifier", "cursor", "filter"], - async = true, - arrow = true, - }: { - params?: readonly string[]; - async?: boolean; - arrow?: boolean; - } = {}, -): string => { - const paramsString = params.join(", "); - const asyncKeyword = async ? "async" : ""; - const [funcKeyword, arrowSymbol] = arrow ? ["", "=>"] : ["function", ""]; - - return ` - federation.${properties[name].setter}( - "/users/{identifier}/${name}", - ${asyncKeyword} ${funcKeyword}(${paramsString}) ${arrowSymbol} { - return { items: [] }; - } - ); - `; -}; +test( + `${ruleName}: ✅ Good - other collection dispatchers without filter are NOT checked`, + () => + otherDispatchers.forEach((name) => { + const paramsString = filterless.join(", "); + testDenoLint({ + code: ` + federation.${properties[name].setter}( + "/users/{identifier}/${name}", + async (${paramsString}) => { + return { items: [] }; + } + ); + `, + rule, + ruleName, + // No expectedError - these should pass without filter + }); + }), +); diff --git a/packages/lint/src/tests/edge-cases.test.ts b/packages/lint/src/tests/edge-cases.test.ts deleted file mode 100644 index 3db493bf5..000000000 --- a/packages/lint/src/tests/edge-cases.test.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { test } from "node:test"; -import { properties } from "../lib/const.ts"; -import { actorPropertyRequired } from "../lib/messages.ts"; -import { testDenoLint } from "../lib/test.ts"; -import { - ACTOR_ID_REQUIRED as ruleName, - default as rule, -} from "../rules/actor-id-required.ts"; - -const expectedError = actorPropertyRequired(properties.id.name); - -const actorProperties = [ - "id", - "following", - "followers", - "outbox", - "inbox", - "liked", - "featured", - "featuredTags", -] as const satisfies (keyof typeof properties)[]; - -const createActorDispatcherCode = ( - name: keyof typeof properties, - { - returnCode, - preReturnCode = "", - }: { - returnCode: string; - preReturnCode?: string; - }, -): string => ` - federation.${properties.id.setter}("/users/{identifier}", async (ctx, identifier) => { - ${preReturnCode} - return ${returnCode}; - }); -`; - -const withId = (extra = "") => - `new Person({ id: ctx.getActorUri(identifier), name: "User"${extra} })`; -const withoutId = () => `new Person({ name: "User" })`; - -const createTestCode = ( - returnCode: string, - preReturnCode = "", - error?: string, -) => -() => - actorProperties.forEach((name) => - testDenoLint({ - code: createActorDispatcherCode(name, { returnCode, preReturnCode }), - rule, - ruleName, - expectedError: error, - }) - ); - -test( - `${ruleName}: ✅ Good - multiple return statements - all have id`, - createTestCode( - ` - identifier === "admin" - ? ${withId()} - : ${withId()} - `.trim().replace(/\n\s*/g, " "), - ` - if (identifier === "admin") { - return ${withId()}; - } - `, - ), -); - -test( - `${ruleName}: ✅ Good - multiple return statements - first missing id (known limitation)`, - createTestCode( - withId(), - ` - if (identifier === "admin") { - return ${withoutId()}; - } - `, - ), -); - -test( - `${ruleName}: ❌ Bad - multiple return statements - second missing id`, - createTestCode( - withoutId(), - ` - if (identifier === "admin") { - return ${withId()}; - } - `, - expectedError, - ), -); - -test( - `${ruleName}: ✅ Good - if/else with else block (known limitation: else not checked)`, - createTestCode( - withId(), - ` - if (identifier) { - return ${withId()}; - } else { - return ${withoutId()}; - } - `, - ), -); - -test( - `${ruleName}: ✅ Good - nested if with id`, - createTestCode( - withId(), - ` - if (identifier) { - if (identifier === "admin") { - return ${withId()}; - } - return ${withId()}; - } - `, - ), -); - -test( - `${ruleName}: ✅ Good - ternary operator with id in both branches`, - createTestCode(`identifier ? ${withId()} : ${withId()}`, ""), -); - -test( - `${ruleName}: ❌ Bad - ternary operator without id in consequent`, - createTestCode( - `identifier ? ${withoutId()} : ${withId()}`, - "", - expectedError, - ), -); - -test( - `${ruleName}: ❌ Bad - ternary operator without id in alternate`, - createTestCode( - `identifier ? ${withId()} : ${withoutId()}`, - "", - expectedError, - ), -); - -test( - `${ruleName}: ✅ Good - spread operator with id property after spread`, - createTestCode( - `new Person({ ...base, id: ctx.getActorUri(identifier) })`, - `const base = { name: "User" };`, - ), -); - -test( - `${ruleName}: ❌ Bad - spread operator with id in spread source (known limitation)`, - createTestCode( - `new Person({ ...base })`, - `const base = { id: ctx.getActorUri(identifier), name: "User" };`, - expectedError, - ), -); - -test( - `${ruleName}: ❌ Bad - variable assignment then return (known limitation)`, - createTestCode( - `actor`, - `const actor = ${withId()};`, - expectedError, - ), -); - -test( - `${ruleName}: ❌ Bad - property assignment after construction (known limitation)`, - createTestCode( - `actor`, - `const actor = ${withoutId()};\n actor.id = ctx.getActorUri(identifier);`, - expectedError, - ), -); - -test(`${ruleName}: ✅ Good - arrow function direct return NewExpression`, () => - actorProperties.forEach((name) => - testDenoLint({ - code: ` - federation.${properties.id.setter}("/users/{identifier}", async (ctx, identifier) => - ${ - withId( - `, ${name}: ctx.${properties[name].getter}(identifier)`, - ) - } - ); - `, - rule, - ruleName, - }) - )); - -test( - `${ruleName}: ✅ Good - return null (no actor)`, - createTestCode(withId(), `if (!identifier) return null;`), -); From 0bfa7cabd2ec1e8a07f86f05ce9b330ace1fac31 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Mon, 8 Dec 2025 10:16:01 +0000 Subject: [PATCH 20/41] Created integration test --- packages/lint/src/tests/integration.test.ts | 537 ++++++++++++++++++++ 1 file changed, 537 insertions(+) create mode 100644 packages/lint/src/tests/integration.test.ts diff --git a/packages/lint/src/tests/integration.test.ts b/packages/lint/src/tests/integration.test.ts new file mode 100644 index 000000000..290e45436 --- /dev/null +++ b/packages/lint/src/tests/integration.test.ts @@ -0,0 +1,537 @@ +/** + * Integration tests for all Fedify lint rules. + * Based on the example code in examples/lint/deno/mod.ts + * + * All tests start from the complete valid code and modify only the part + * necessary to trigger the specific lint rule being tested. + */ +import { assert, assertEquals } from "jsr:@std/assert"; +import { test } from "node:test"; +import plugin from "../mod.ts"; + +const PLUGIN_NAME = "fedify-lint"; + +/** + * Run all lint rules on the given code and return diagnostics. + */ +function lintCode(code: string): Deno.lint.Diagnostic[] { + return Deno.lint.runPlugin(plugin, "integration.test.ts", code); +} + +/** + * Assert that the code passes all lint rules (no diagnostics). + */ +function assertNoErrors(code: string, message?: string) { + const diagnostics = lintCode(code); + assertEquals( + diagnostics.length, + 0, + message ?? + `Expected no errors but got: ${ + diagnostics.map((d) => `${d.id}: ${d.message}`).join(", ") + }`, + ); +} + +/** + * Assert that the code has exactly one error matching the given rule. + */ +function assertHasError(code: string, ruleName: string, message?: string) { + const diagnostics = lintCode(code); + const ruleId = `${PLUGIN_NAME}/${ruleName}`; + const matched = diagnostics.some((d) => d.id === ruleId); + assert( + matched, + message ?? + `Expected error from ${ruleName} but got: ${ + diagnostics.length === 0 + ? "no errors" + : diagnostics.map((d) => d.id).join(", ") + }`, + ); +} + +// ============================================================================= +// Complete Valid Code (based on examples/lint/deno/mod.ts) +// ============================================================================= + +/** + * Complete valid code that passes all lint rules. + * This is the baseline for all tests - each test modifies only what's needed. + */ +const COMPLETE_VALID_CODE = ` +import { + createFederation, + Endpoints, + InProcessMessageQueue, + MemoryKvStore, + Person, +} from "@fedify/fedify"; + +const federation = createFederation({ + kv: new MemoryKvStore(), + queue: new InProcessMessageQueue(), +}); + +federation + .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + const keyPairs = await ctx.getActorKeyPairs(identifier); + return new Person({ + id: ctx.getActorUri(identifier), + name: "John Doe", + summary: "A test actor for comprehensive lint rule validation", + inbox: ctx.getInboxUri(identifier), + endpoints: new Endpoints({ + sharedInbox: ctx.getInboxUri(), + }), + outbox: ctx.getOutboxUri(identifier), + following: ctx.getFollowingUri(identifier), + followers: ctx.getFollowersUri(identifier), + liked: ctx.getLikedUri(identifier), + featured: ctx.getFeaturedUri(identifier), + featuredTags: ctx.getFeaturedTagsUri(identifier), + publicKey: keyPairs[0]?.cryptographicKey, + assertionMethod: keyPairs[0]?.multikey, + }); + }) + .setKeyPairsDispatcher(async (_ctx, _identifier) => []); + +federation.setInboxListeners("/users/{identifier}/inbox", "/inbox"); + +federation.setOutboxDispatcher( + "/users/{identifier}/outbox", + async (_ctx, _identifier, _cursor) => { + return { items: [] }; + }, +); + +federation.setFollowingDispatcher( + "/users/{identifier}/following", + async (_ctx, _identifier, _cursor) => { + return { items: [] }; + }, +); + +federation.setFollowersDispatcher( + "/users/{identifier}/followers", + async (_ctx, _identifier, _cursor, _filter) => { + return { items: [] }; + }, +); + +federation.setLikedDispatcher( + "/users/{identifier}/liked", + async (_ctx, _identifier, _cursor) => { + return { items: [] }; + }, +); + +federation.setFeaturedDispatcher( + "/users/{identifier}/featured", + async (_ctx, _identifier, _cursor) => { + return { items: [] }; + }, +); + +federation.setFeaturedTagsDispatcher( + "/users/{identifier}/tags", + async (_ctx, _identifier, _cursor) => { + return { items: [] }; + }, +); +`; + +// ============================================================================= +// Test: Complete Valid Code +// ============================================================================= + +test("Integration: ✅ Complete valid code passes all rules", () => { + assertNoErrors(COMPLETE_VALID_CODE); +}); + +// ============================================================================= +// Test: *-required rules (property missing when dispatcher is configured) +// ============================================================================= + +test("Integration: ❌ actor-id-required - missing id property", () => { + const code = COMPLETE_VALID_CODE.replace( + "id: ctx.getActorUri(identifier),", + "// id: ctx.getActorUri(identifier), // REMOVED", + ); + assertHasError(code, "actor-id-required"); +}); + +test("Integration: ❌ actor-inbox-property-required - missing inbox property", () => { + const code = COMPLETE_VALID_CODE.replace( + "inbox: ctx.getInboxUri(identifier),", + "// inbox: ctx.getInboxUri(identifier), // REMOVED", + ); + assertHasError(code, "actor-inbox-property-required"); +}); + +test("Integration: ❌ actor-shared-inbox-property-required - missing sharedInbox property", () => { + const code = COMPLETE_VALID_CODE.replace( + `endpoints: new Endpoints({ + sharedInbox: ctx.getInboxUri(), + }),`, + "// endpoints: REMOVED", + ); + assertHasError(code, "actor-shared-inbox-property-required"); +}); + +test("Integration: ❌ actor-following-property-required - missing following property", () => { + const code = COMPLETE_VALID_CODE.replace( + "following: ctx.getFollowingUri(identifier),", + "// following: ctx.getFollowingUri(identifier), // REMOVED", + ); + assertHasError(code, "actor-following-property-required"); +}); + +test("Integration: ❌ actor-followers-property-required - missing followers property", () => { + const code = COMPLETE_VALID_CODE.replace( + "followers: ctx.getFollowersUri(identifier),", + "// followers: ctx.getFollowersUri(identifier), // REMOVED", + ); + assertHasError(code, "actor-followers-property-required"); +}); + +test("Integration: ❌ actor-outbox-property-required - missing outbox property", () => { + const code = COMPLETE_VALID_CODE.replace( + "outbox: ctx.getOutboxUri(identifier),", + "// outbox: ctx.getOutboxUri(identifier), // REMOVED", + ); + assertHasError(code, "actor-outbox-property-required"); +}); + +test("Integration: ❌ actor-liked-property-required - missing liked property", () => { + const code = COMPLETE_VALID_CODE.replace( + "liked: ctx.getLikedUri(identifier),", + "// liked: ctx.getLikedUri(identifier), // REMOVED", + ); + assertHasError(code, "actor-liked-property-required"); +}); + +test("Integration: ❌ actor-featured-property-required - missing featured property", () => { + const code = COMPLETE_VALID_CODE.replace( + "featured: ctx.getFeaturedUri(identifier),", + "// featured: ctx.getFeaturedUri(identifier), // REMOVED", + ); + assertHasError(code, "actor-featured-property-required"); +}); + +test("Integration: ❌ actor-featured-tags-property-required - missing featuredTags property", () => { + const code = COMPLETE_VALID_CODE.replace( + "featuredTags: ctx.getFeaturedTagsUri(identifier),", + "// featuredTags: ctx.getFeaturedTagsUri(identifier), // REMOVED", + ); + assertHasError(code, "actor-featured-tags-property-required"); +}); + +test("Integration: ❌ actor-public-key-required - missing publicKey property", () => { + const code = COMPLETE_VALID_CODE.replace( + "publicKey: keyPairs[0]?.cryptographicKey,", + "// publicKey: keyPairs[0]?.cryptographicKey, // REMOVED", + ); + assertHasError(code, "actor-public-key-required"); +}); + +test("Integration: ❌ actor-assertion-method-required - missing assertionMethod property", () => { + const code = COMPLETE_VALID_CODE.replace( + "assertionMethod: keyPairs[0]?.multikey,", + "// assertionMethod: keyPairs[0]?.multikey, // REMOVED", + ); + assertHasError(code, "actor-assertion-method-required"); +}); + +// ============================================================================= +// Test: *-mismatch rules (property uses wrong context method) +// ============================================================================= + +test("Integration: ❌ actor-id-mismatch - id uses wrong context method", () => { + const code = COMPLETE_VALID_CODE.replace( + "id: ctx.getActorUri(identifier),", + "id: ctx.getInboxUri(identifier), // WRONG METHOD", + ); + assertHasError(code, "actor-id-mismatch"); +}); + +test("Integration: ❌ actor-inbox-property-mismatch - inbox uses wrong context method", () => { + const code = COMPLETE_VALID_CODE.replace( + "inbox: ctx.getInboxUri(identifier),", + "inbox: ctx.getOutboxUri(identifier), // WRONG METHOD", + ); + assertHasError(code, "actor-inbox-property-mismatch"); +}); + +test("Integration: ❌ actor-shared-inbox-property-mismatch - sharedInbox uses wrong context method", () => { + const code = COMPLETE_VALID_CODE.replace( + "sharedInbox: ctx.getInboxUri(),", + "sharedInbox: ctx.getOutboxUri(identifier), // WRONG METHOD", + ); + assertHasError(code, "actor-shared-inbox-property-mismatch"); +}); + +test("Integration: ❌ actor-following-property-mismatch - following uses wrong context method", () => { + const code = COMPLETE_VALID_CODE.replace( + "following: ctx.getFollowingUri(identifier),", + "following: ctx.getFollowersUri(identifier), // WRONG METHOD", + ); + assertHasError(code, "actor-following-property-mismatch"); +}); + +test("Integration: ❌ actor-followers-property-mismatch - followers uses wrong context method", () => { + const code = COMPLETE_VALID_CODE.replace( + "followers: ctx.getFollowersUri(identifier),", + "followers: ctx.getFollowingUri(identifier), // WRONG METHOD", + ); + assertHasError(code, "actor-followers-property-mismatch"); +}); + +test("Integration: ❌ actor-outbox-property-mismatch - outbox uses wrong context method", () => { + const code = COMPLETE_VALID_CODE.replace( + "outbox: ctx.getOutboxUri(identifier),", + "outbox: ctx.getInboxUri(identifier), // WRONG METHOD", + ); + assertHasError(code, "actor-outbox-property-mismatch"); +}); + +test("Integration: ❌ actor-liked-property-mismatch - liked uses wrong context method", () => { + const code = COMPLETE_VALID_CODE.replace( + "liked: ctx.getLikedUri(identifier),", + "liked: ctx.getOutboxUri(identifier), // WRONG METHOD", + ); + assertHasError(code, "actor-liked-property-mismatch"); +}); + +test("Integration: ❌ actor-featured-property-mismatch - featured uses wrong context method", () => { + const code = COMPLETE_VALID_CODE.replace( + "featured: ctx.getFeaturedUri(identifier),", + "featured: ctx.getOutboxUri(identifier), // WRONG METHOD", + ); + assertHasError(code, "actor-featured-property-mismatch"); +}); + +test("Integration: ❌ actor-featured-tags-property-mismatch - featuredTags uses wrong context method", () => { + const code = COMPLETE_VALID_CODE.replace( + "featuredTags: ctx.getFeaturedTagsUri(identifier),", + "featuredTags: ctx.getOutboxUri(identifier), // WRONG METHOD", + ); + assertHasError(code, "actor-featured-tags-property-mismatch"); +}); + +// ============================================================================= +// Test: collection-filtering-not-implemented +// ============================================================================= + +test("Integration: ❌ collection-filtering-not-implemented - setFollowersDispatcher without filter parameter", () => { + const code = COMPLETE_VALID_CODE.replace( + "async (_ctx, _identifier, _cursor, _filter) => {", + "async (_ctx, _identifier, _cursor) => { // NO FILTER", + ); + assertHasError(code, "collection-filtering-not-implemented"); +}); + +// ============================================================================= +// Test: Non-Federation objects should not trigger errors +// ============================================================================= + +test("Integration: ✅ Non-Federation object - custom federation object", () => { + const code = COMPLETE_VALID_CODE.replace( + `const federation = createFederation({ + kv: new MemoryKvStore(), + queue: new InProcessMessageQueue(), +});`, + `const federation = { + setActorDispatcher: () => ({ setKeyPairsDispatcher: () => {} }), + setInboxListeners: () => {}, + setOutboxDispatcher: () => {}, + setFollowingDispatcher: () => {}, + setFollowersDispatcher: () => {}, + setLikedDispatcher: () => {}, + setFeaturedDispatcher: () => {}, + setFeaturedTagsDispatcher: () => {}, +};`, + ); + assertNoErrors(code); +}); + +// ============================================================================= +// Test: Dispatcher not configured - property not required +// ============================================================================= + +test("Integration: ✅ No setFollowingDispatcher - following property not required", () => { + // Remove the following property AND the setFollowingDispatcher + const code = COMPLETE_VALID_CODE + .replace( + "following: ctx.getFollowingUri(identifier),", + "// following: REMOVED", + ) + .replace( + `federation.setFollowingDispatcher( + "/users/{identifier}/following", + async (_ctx, _identifier, _cursor) => { + return { items: [] }; + }, +);`, + "// setFollowingDispatcher REMOVED", + ); + assertNoErrors(code); +}); + +test("Integration: ✅ No setFollowersDispatcher - followers property not required", () => { + const code = COMPLETE_VALID_CODE + .replace( + "followers: ctx.getFollowersUri(identifier),", + "// followers: REMOVED", + ) + .replace( + `federation.setFollowersDispatcher( + "/users/{identifier}/followers", + async (_ctx, _identifier, _cursor, _filter) => { + return { items: [] }; + }, +);`, + "// setFollowersDispatcher REMOVED", + ); + assertNoErrors(code); +}); + +test("Integration: ✅ No setOutboxDispatcher - outbox property not required", () => { + const code = COMPLETE_VALID_CODE + .replace( + "outbox: ctx.getOutboxUri(identifier),", + "// outbox: REMOVED", + ) + .replace( + `federation.setOutboxDispatcher( + "/users/{identifier}/outbox", + async (_ctx, _identifier, _cursor) => { + return { items: [] }; + }, +);`, + "// setOutboxDispatcher REMOVED", + ); + assertNoErrors(code); +}); + +test("Integration: ✅ No setLikedDispatcher - liked property not required", () => { + const code = COMPLETE_VALID_CODE + .replace( + "liked: ctx.getLikedUri(identifier),", + "// liked: REMOVED", + ) + .replace( + `federation.setLikedDispatcher( + "/users/{identifier}/liked", + async (_ctx, _identifier, _cursor) => { + return { items: [] }; + }, +);`, + "// setLikedDispatcher REMOVED", + ); + assertNoErrors(code); +}); + +test("Integration: ✅ No setFeaturedDispatcher - featured property not required", () => { + const code = COMPLETE_VALID_CODE + .replace( + "featured: ctx.getFeaturedUri(identifier),", + "// featured: REMOVED", + ) + .replace( + `federation.setFeaturedDispatcher( + "/users/{identifier}/featured", + async (_ctx, _identifier, _cursor) => { + return { items: [] }; + }, +);`, + "// setFeaturedDispatcher REMOVED", + ); + assertNoErrors(code); +}); + +test("Integration: ✅ No setFeaturedTagsDispatcher - featuredTags property not required", () => { + const code = COMPLETE_VALID_CODE + .replace( + "featuredTags: ctx.getFeaturedTagsUri(identifier),", + "// featuredTags: REMOVED", + ) + .replace( + `federation.setFeaturedTagsDispatcher( + "/users/{identifier}/tags", + async (_ctx, _identifier, _cursor) => { + return { items: [] }; + }, +);`, + "// setFeaturedTagsDispatcher REMOVED", + ); + assertNoErrors(code); +}); + +test("Integration: ✅ No setInboxListeners - inbox/sharedInbox not required", () => { + const code = COMPLETE_VALID_CODE + .replace( + "inbox: ctx.getInboxUri(identifier),", + "// inbox: REMOVED", + ) + .replace( + `endpoints: new Endpoints({ + sharedInbox: ctx.getInboxUri(), + }),`, + "// endpoints: REMOVED", + ) + .replace( + `federation.setInboxListeners("/users/{identifier}/inbox", "/inbox");`, + "// setInboxListeners REMOVED", + ); + assertNoErrors(code); +}); + +test("Integration: ✅ No setKeyPairsDispatcher - publicKey/assertionMethod not required", () => { + const code = COMPLETE_VALID_CODE + .replace( + "const keyPairs = await ctx.getActorKeyPairs(identifier);", + "// keyPairs REMOVED", + ) + .replace( + "publicKey: keyPairs[0]?.cryptographicKey,", + "// publicKey: REMOVED", + ) + .replace( + "assertionMethod: keyPairs[0]?.multikey,", + "// assertionMethod: REMOVED", + ) + .replace( + ".setKeyPairsDispatcher(async (_ctx, _identifier) => []);", + "; // setKeyPairsDispatcher REMOVED", + ); + assertNoErrors(code); +}); + +// ============================================================================= +// Test: Multiple errors in one file +// ============================================================================= + +test("Integration: ❌ Multiple errors - missing id and inbox", () => { + const code = COMPLETE_VALID_CODE + .replace( + "id: ctx.getActorUri(identifier),", + "// id: REMOVED", + ) + .replace( + "inbox: ctx.getInboxUri(identifier),", + "// inbox: REMOVED", + ); + + const diagnostics = lintCode(code); + const ruleIds = diagnostics.map((d) => d.id); + + assert( + ruleIds.includes(`${PLUGIN_NAME}/actor-id-required`), + "Expected actor-id-required error", + ); + assert( + ruleIds.includes(`${PLUGIN_NAME}/actor-inbox-property-required`), + "Expected actor-inbox-property-required error", + ); +}); From a347e197c8963358eb22a1e5a6f8df171314c7f5 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Mon, 8 Dec 2025 10:23:04 +0000 Subject: [PATCH 21/41] Initiated for eslint plugin --- packages/lint/package.json | 64 ++++++++++++++++++++++++++++++++++ packages/lint/src/index.ts | 0 packages/lint/tsdown.config.ts | 8 +++++ 3 files changed, 72 insertions(+) create mode 100644 packages/lint/package.json create mode 100644 packages/lint/src/index.ts create mode 100644 packages/lint/tsdown.config.ts diff --git a/packages/lint/package.json b/packages/lint/package.json new file mode 100644 index 000000000..2a3eab18e --- /dev/null +++ b/packages/lint/package.json @@ -0,0 +1,64 @@ +{ + "name": "@fedify/lint", + "version": "2.0.0", + "description": "Fedify linting rules and plugins", + "keywords": [ + "Fedify", + "ActivityPub", + "Fediverse", + "Lint" + ], + "author": { + "name": "Chanhaeng Lee", + "email": "2chanhaeng@gmail.com", + "url": "https://chomu.dev/" + }, + "homepage": "https://fedify.dev/", + "repository": { + "type": "git", + "url": "git+https://github.com/fedify-dev/fedify.git", + "directory": "packages/lint" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/fedify-dev/fedify/issues" + }, + "funding": [ + "https://opencollective.com/fedify", + "https://github.com/sponsors/dahlia" + ], + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": { + "import": "./dist/index.d.ts", + "require": "./dist/index.d.cts", + "default": "./dist/index.d.ts" + }, + "import": "./dist/index.js", + "require": "./dist/index.cjs", + "default": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "dist/", + "package.json" + ], + "peerDependencies": { + "@fedify/fedify": "workspace:^" + }, + "devDependencies": { + "tsdown": "catalog:", + "typescript": "catalog:" + }, + "scripts": { + "build": "tsdown", + "prepack": "tsdown", + "prepublish": "tsdown", + "test": "deno task codegen && tsdown && cd dist/ && node --test" + } +} diff --git a/packages/lint/src/index.ts b/packages/lint/src/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/lint/tsdown.config.ts b/packages/lint/tsdown.config.ts new file mode 100644 index 000000000..70a57795c --- /dev/null +++ b/packages/lint/tsdown.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "tsdown"; + +export default defineConfig({ + entry: ["src/index.ts"], + dts: true, + format: ["esm", "cjs"], + platform: "node", +}); From 0dc434a1968474f4228626a73bb053c4c5085481 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Tue, 9 Dec 2025 05:05:31 +0000 Subject: [PATCH 22/41] Added linter for eslint --- packages/lint/deno.json | 6 +- packages/lint/package.json | 31 +- packages/lint/src/eslint-rules.ts | 360 +++++++++ packages/lint/src/eslint.ts | 199 +++++ packages/lint/src/index.ts | 34 + packages/lint/src/lib/ast-types.ts | 51 ++ packages/lint/src/lib/common-pred.ts | 131 +++ packages/lint/src/lib/common-tracker.ts | 59 ++ packages/lint/src/lib/eslint-rule-factory.ts | 754 ++++++++++++++++++ packages/lint/src/lib/eslint-types.ts | 141 ++++ .../lint/src/lib/property-checker-eslint.ts | 283 +++++++ packages/lint/tsdown.config.ts | 3 +- 12 files changed, 2040 insertions(+), 12 deletions(-) create mode 100644 packages/lint/src/eslint-rules.ts create mode 100644 packages/lint/src/eslint.ts create mode 100644 packages/lint/src/lib/ast-types.ts create mode 100644 packages/lint/src/lib/common-pred.ts create mode 100644 packages/lint/src/lib/common-tracker.ts create mode 100644 packages/lint/src/lib/eslint-rule-factory.ts create mode 100644 packages/lint/src/lib/eslint-types.ts create mode 100644 packages/lint/src/lib/property-checker-eslint.ts diff --git a/packages/lint/deno.json b/packages/lint/deno.json index f7ed9d990..a31a1d85c 100644 --- a/packages/lint/deno.json +++ b/packages/lint/deno.json @@ -3,7 +3,11 @@ "version": "2.0.0", "license": "MIT", "exports": { - ".": "./src/mod.ts" + ".": "./src/mod.ts", + "./eslint": "./src/eslint.ts" + }, + "imports": { + "@typescript-eslint/utils": "npm:@typescript-eslint/utils@^8.0.0" }, "tasks": { "test": "deno test --allow-all" diff --git a/packages/lint/package.json b/packages/lint/package.json index 2a3eab18e..c69e3f92e 100644 --- a/packages/lint/package.json +++ b/packages/lint/package.json @@ -6,7 +6,9 @@ "Fedify", "ActivityPub", "Fediverse", - "Lint" + "Lint", + "ESLint", + "ESLint Plugin" ], "author": { "name": "Chanhaeng Lee", @@ -30,17 +32,15 @@ "type": "module", "main": "./dist/index.cjs", "module": "./dist/index.js", - "types": "./dist/index.d.ts", + "types": "./dist/index.d.cts", "exports": { ".": { - "types": { - "import": "./dist/index.d.ts", - "require": "./dist/index.d.cts", - "default": "./dist/index.d.ts" - }, "import": "./dist/index.js", - "require": "./dist/index.cjs", - "default": "./dist/index.js" + "require": "./dist/index.cjs" + }, + "./eslint": { + "import": "./dist/eslint.js", + "require": "./dist/eslint.cjs" }, "./package.json": "./package.json" }, @@ -49,9 +49,20 @@ "package.json" ], "peerDependencies": { - "@fedify/fedify": "workspace:^" + "@fedify/fedify": "workspace:^", + "eslint": ">=8.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + }, + "dependencies": { + "@fxts/core": "catalog:", + "@typescript-eslint/utils": "^8.0.0" }, "devDependencies": { + "@types/eslint": "^9.0.0", "tsdown": "catalog:", "typescript": "catalog:" }, diff --git a/packages/lint/src/eslint-rules.ts b/packages/lint/src/eslint-rules.ts new file mode 100644 index 000000000..ff1d74b17 --- /dev/null +++ b/packages/lint/src/eslint-rules.ts @@ -0,0 +1,360 @@ +/** + * ESLint rule factories for Fedify lint rules. + * Uses TSESTree types from @typescript-eslint/utils. + */ +import type { TSESLint, TSESTree } from "@typescript-eslint/utils"; +import { + actorPropertyMismatch, + actorPropertyRequired, + COLLECTION_FILTERING_NOT_IMPLEMENTED_ERROR, +} from "./lib/messages.ts"; +import { + createPropertyExistenceChecker, + createPropertyValueChecker, + extractFunctionParams, + hasMinParams, + searchFunctionBody, +} from "./lib/property-checker-eslint.ts"; +import type { MethodCallContext, PropertyConfig } from "./lib/types.ts"; + +// ============================================================================ +// Types +// ============================================================================ + +type RuleContext = TSESLint.RuleContext; +type RuleModule = TSESLint.RuleModule; +type CallExpression = TSESTree.CallExpression; +type MemberExpression = TSESTree.MemberExpression; +type Identifier = TSESTree.Identifier; +type VariableDeclarator = TSESTree.VariableDeclarator; +type Expression = TSESTree.Expression; +type FunctionNode = + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionExpression; + +// ============================================================================ +// Federation Variable Tracker +// ============================================================================ + +interface FederationTracker { + handleVariableDeclarator(node: VariableDeclarator): void; + isFederationObject(node: Expression): boolean; +} + +function createFederationTracker(): FederationTracker { + const federationVariables = new Set(); + + const isCreateFederationCall = (node: CallExpression): boolean => + node.callee.type === "Identifier" && + /^create(Federation|FederationBuilder)$/i.test(node.callee.name); + + const isFederationObject = (node: Expression): boolean => { + switch (node.type) { + case "Identifier": + return federationVariables.has(node.name); + case "CallExpression": + if (isCreateFederationCall(node)) return true; + if (node.callee.type === "MemberExpression") { + return isFederationObject(node.callee.object); + } + return false; + case "MemberExpression": + return isFederationObject(node.object); + default: + return false; + } + }; + + return { + handleVariableDeclarator(node: VariableDeclarator): void { + if ( + node.init?.type === "CallExpression" && + isCreateFederationCall(node.init) && + node.id.type === "Identifier" + ) { + federationVariables.add(node.id.name); + } + }, + isFederationObject, + }; +} + +// ============================================================================ +// CallExpression Helpers +// ============================================================================ + +interface CallMemberExpression extends CallExpression { + callee: MemberExpression & { property: Identifier }; +} + +function isCallMemberExpression( + node: CallExpression, +): node is CallMemberExpression { + return ( + node.callee.type === "MemberExpression" && + node.callee.property.type === "Identifier" + ); +} + +function hasMethodName( + node: CallMemberExpression, + name: string, +): boolean { + return node.callee.property.name === name; +} + +function isSetActorDispatcherCall( + node: CallExpression, +): node is CallMemberExpression { + return ( + isCallMemberExpression(node) && + hasMethodName(node, "setActorDispatcher") && + node.arguments.length >= 2 + ); +} + +function isSetFollowersDispatcherCall( + node: CallExpression, +): node is CallMemberExpression { + return ( + isCallMemberExpression(node) && + hasMethodName(node, "setFollowersDispatcher") && + node.arguments.length >= 2 + ); +} + +function isFunction(node: TSESTree.Node): node is FunctionNode { + return ( + node.type === "ArrowFunctionExpression" || + node.type === "FunctionExpression" + ); +} + +// ============================================================================ +// Dispatcher Tracker +// ============================================================================ + +interface DispatcherTracker { + isConfigured(): boolean; + checkCall(node: CallExpression, tracker: FederationTracker): void; +} + +function createDispatcherTracker(methodName: string): DispatcherTracker { + let configured = false; + + return { + isConfigured: () => configured, + checkCall(node: CallExpression, tracker: FederationTracker): void { + if ( + isCallMemberExpression(node) && + hasMethodName(node, methodName) && + tracker.isFederationObject(node.callee.object) + ) { + configured = true; + } + }, + }; +} + +// ============================================================================ +// Actor Dispatcher Info +// ============================================================================ + +interface ActorDispatcherInfo { + node: CallMemberExpression; + dispatcherFn: FunctionNode; +} + +// ============================================================================ +// ESLint Rule Factory: Required Rules +// ============================================================================ + +export function createRequiredRule( + _ruleId: string, + config: PropertyConfig, +): RuleModule { + return { + meta: { + type: "suggestion", + docs: { + description: `Ensure actor dispatcher returns ${ + config.path.join(".") + } property`, + }, + schema: [], + messages: { + required: "{{ message }}", + }, + }, + defaultOptions: [], + create(context: RuleContext) { + const federationTracker = createFederationTracker(); + const dispatcherTracker = createDispatcherTracker(config.setter); + const actorDispatchers: ActorDispatcherInfo[] = []; + + const propertyChecker = createPropertyExistenceChecker(config.path); + + return { + VariableDeclarator(node: VariableDeclarator): void { + federationTracker.handleVariableDeclarator(node); + }, + + CallExpression(node: CallExpression): void { + dispatcherTracker.checkCall(node, federationTracker); + + if (!isSetActorDispatcherCall(node)) return; + if (!federationTracker.isFederationObject(node.callee.object)) return; + + const dispatcherArg = node.arguments[1]; + if (isFunction(dispatcherArg)) { + actorDispatchers.push({ + node, + dispatcherFn: dispatcherArg, + }); + } + }, + + "Program:exit"(): void { + if (!dispatcherTracker.isConfigured()) return; + + for (const { dispatcherFn } of actorDispatchers) { + const hasProperty = searchFunctionBody(propertyChecker)( + dispatcherFn.body, + ); + + if (!hasProperty) { + context.report({ + node: dispatcherFn, + messageId: "required", + data: { message: actorPropertyRequired(config) }, + }); + } + } + }, + }; + }, + }; +} + +// ============================================================================ +// ESLint Rule Factory: Mismatch Rules +// ============================================================================ + +export function createMismatchRule( + _ruleId: string, + config: PropertyConfig, +): RuleModule { + return { + meta: { + type: "problem", + docs: { + description: `Ensure actor's ${ + config.path.join(".") + } property uses correct context method`, + }, + schema: [], + messages: { + mismatch: "{{ message }}", + }, + }, + defaultOptions: [], + create(context: RuleContext) { + const federationTracker = createFederationTracker(); + + return { + VariableDeclarator(node: VariableDeclarator): void { + federationTracker.handleVariableDeclarator(node); + }, + + CallExpression(node: CallExpression): void { + if (!isSetActorDispatcherCall(node)) return; + if (!federationTracker.isFederationObject(node.callee.object)) return; + + const dispatcherArg = node.arguments[1]; + if (!isFunction(dispatcherArg)) return; + + const [ctxName, idName] = extractFunctionParams(dispatcherArg); + if (!ctxName || !idName) return; + + const methodCallContext: MethodCallContext = { + path: config.path.join("."), + ctxName, + idName, + methodName: config.getter, + requiresIdentifier: config.requiresIdentifier, + }; + + // Check if property exists + const existenceChecker = createPropertyExistenceChecker(config.path); + const hasProperty = searchFunctionBody(existenceChecker)( + dispatcherArg.body, + ); + + if (!hasProperty) return; // Let required rule handle this + + // Check if property has correct value + const valueChecker = createPropertyValueChecker( + config.path, + methodCallContext, + ); + const hasCorrectValue = searchFunctionBody(valueChecker)( + dispatcherArg.body, + ); + + if (!hasCorrectValue) { + context.report({ + node: dispatcherArg, + messageId: "mismatch", + data: { message: actorPropertyMismatch(methodCallContext) }, + }); + } + }, + }; + }, + }; +} + +// ============================================================================ +// ESLint Rule Factory: Collection Filtering +// ============================================================================ + +export function createCollectionFilteringRule(_ruleId: string): RuleModule { + return { + meta: { + type: "suggestion", + docs: { + description: "Ensure followers dispatcher implements filtering", + }, + schema: [], + messages: { + filterRequired: COLLECTION_FILTERING_NOT_IMPLEMENTED_ERROR, + }, + }, + defaultOptions: [], + create(context: RuleContext) { + const federationTracker = createFederationTracker(); + + return { + VariableDeclarator(node: VariableDeclarator): void { + federationTracker.handleVariableDeclarator(node); + }, + + CallExpression(node: CallExpression): void { + if (!isSetFollowersDispatcherCall(node)) return; + if (!federationTracker.isFederationObject(node.callee.object)) return; + + const dispatcherArg = node.arguments[1]; + if (!isFunction(dispatcherArg)) return; + + // Filter is the 4th parameter (index 3) + if (!hasMinParams(4)(dispatcherArg)) { + context.report({ + node: dispatcherArg, + messageId: "filterRequired", + }); + } + }, + }; + }, + }; +} diff --git a/packages/lint/src/eslint.ts b/packages/lint/src/eslint.ts new file mode 100644 index 000000000..7ae556a66 --- /dev/null +++ b/packages/lint/src/eslint.ts @@ -0,0 +1,199 @@ +/** + * ESLint plugin for Fedify. + * Provides lint rules for validating Fedify federation code. + */ +import { pipe } from "@fxts/core"; +import type { TSESLint } from "@typescript-eslint/utils"; +import { + createCollectionFilteringRule, + createMismatchRule, + createRequiredRule, +} from "./eslint-rules.ts"; +import { properties } from "./lib/const.ts"; + +// ============================================================================ +// Rule IDs +// ============================================================================ + +const RULE_IDS = { + // Required rules + actorIdRequired: "actor-id-required", + actorFollowingPropertyRequired: "actor-following-property-required", + actorFollowersPropertyRequired: "actor-followers-property-required", + actorOutboxPropertyRequired: "actor-outbox-property-required", + actorLikedPropertyRequired: "actor-liked-property-required", + actorFeaturedPropertyRequired: "actor-featured-property-required", + actorFeaturedTagsPropertyRequired: "actor-featured-tags-property-required", + actorInboxPropertyRequired: "actor-inbox-property-required", + actorSharedInboxPropertyRequired: "actor-shared-inbox-property-required", + actorPublicKeyRequired: "actor-public-key-required", + actorAssertionMethodRequired: "actor-assertion-method-required", + + // Mismatch rules + actorIdMismatch: "actor-id-mismatch", + actorFollowingPropertyMismatch: "actor-following-property-mismatch", + actorFollowersPropertyMismatch: "actor-followers-property-mismatch", + actorOutboxPropertyMismatch: "actor-outbox-property-mismatch", + actorLikedPropertyMismatch: "actor-liked-property-mismatch", + actorFeaturedPropertyMismatch: "actor-featured-property-mismatch", + actorFeaturedTagsPropertyMismatch: "actor-featured-tags-property-mismatch", + actorInboxPropertyMismatch: "actor-inbox-property-mismatch", + actorSharedInboxPropertyMismatch: "actor-shared-inbox-property-mismatch", + + // Collection rules + collectionFilteringNotImplemented: "collection-filtering-not-implemented", +} as const; + +// ============================================================================ +// Rule Definitions +// ============================================================================ + +const rules: Record> = { + // Required rules + [RULE_IDS.actorIdRequired]: createRequiredRule( + RULE_IDS.actorIdRequired, + properties.id, + ), + [RULE_IDS.actorFollowingPropertyRequired]: createRequiredRule( + RULE_IDS.actorFollowingPropertyRequired, + properties.following, + ), + [RULE_IDS.actorFollowersPropertyRequired]: createRequiredRule( + RULE_IDS.actorFollowersPropertyRequired, + properties.followers, + ), + [RULE_IDS.actorOutboxPropertyRequired]: createRequiredRule( + RULE_IDS.actorOutboxPropertyRequired, + properties.outbox, + ), + [RULE_IDS.actorLikedPropertyRequired]: createRequiredRule( + RULE_IDS.actorLikedPropertyRequired, + properties.liked, + ), + [RULE_IDS.actorFeaturedPropertyRequired]: createRequiredRule( + RULE_IDS.actorFeaturedPropertyRequired, + properties.featured, + ), + [RULE_IDS.actorFeaturedTagsPropertyRequired]: createRequiredRule( + RULE_IDS.actorFeaturedTagsPropertyRequired, + properties.featuredTags, + ), + [RULE_IDS.actorInboxPropertyRequired]: createRequiredRule( + RULE_IDS.actorInboxPropertyRequired, + properties.inbox, + ), + [RULE_IDS.actorSharedInboxPropertyRequired]: createRequiredRule( + RULE_IDS.actorSharedInboxPropertyRequired, + properties.sharedInbox, + ), + [RULE_IDS.actorPublicKeyRequired]: createRequiredRule( + RULE_IDS.actorPublicKeyRequired, + properties.publicKey, + ), + [RULE_IDS.actorAssertionMethodRequired]: createRequiredRule( + RULE_IDS.actorAssertionMethodRequired, + properties.assertionMethod, + ), + + // Mismatch rules + [RULE_IDS.actorIdMismatch]: createMismatchRule( + RULE_IDS.actorIdMismatch, + properties.id, + ), + [RULE_IDS.actorFollowingPropertyMismatch]: createMismatchRule( + RULE_IDS.actorFollowingPropertyMismatch, + properties.following, + ), + [RULE_IDS.actorFollowersPropertyMismatch]: createMismatchRule( + RULE_IDS.actorFollowersPropertyMismatch, + properties.followers, + ), + [RULE_IDS.actorOutboxPropertyMismatch]: createMismatchRule( + RULE_IDS.actorOutboxPropertyMismatch, + properties.outbox, + ), + [RULE_IDS.actorLikedPropertyMismatch]: createMismatchRule( + RULE_IDS.actorLikedPropertyMismatch, + properties.liked, + ), + [RULE_IDS.actorFeaturedPropertyMismatch]: createMismatchRule( + RULE_IDS.actorFeaturedPropertyMismatch, + properties.featured, + ), + [RULE_IDS.actorFeaturedTagsPropertyMismatch]: createMismatchRule( + RULE_IDS.actorFeaturedTagsPropertyMismatch, + properties.featuredTags, + ), + [RULE_IDS.actorInboxPropertyMismatch]: createMismatchRule( + RULE_IDS.actorInboxPropertyMismatch, + properties.inbox, + ), + [RULE_IDS.actorSharedInboxPropertyMismatch]: createMismatchRule( + RULE_IDS.actorSharedInboxPropertyMismatch, + properties.sharedInbox, + ), + + // Collection rules + [RULE_IDS.collectionFilteringNotImplemented]: createCollectionFilteringRule( + RULE_IDS.collectionFilteringNotImplemented, + ), +}; + +// ============================================================================ +// Plugin Configuration +// ============================================================================ + +/** + * Recommended configuration - enables all rules as warnings + */ +/** + * Recommended configuration - enables all rules as warnings + */ +const recommendedRules = pipe( + Object.keys(rules), + (keys) => + keys.reduce((acc, key) => { + acc[`@fedify/lint/${key}`] = "warn" as const; + return acc; + }, {} as Record), +); + +/** + * Strict configuration - enables all rules as errors + */ +const strictRules = pipe( + Object.keys(rules), + (keys) => + keys.reduce((acc, key) => { + acc[`@fedify/lint/${key}`] = "error" as const; + return acc; + }, {} as Record), +); + +// ============================================================================ +// Plugin Export +// ============================================================================ + +const plugin: TSESLint.Linter.Plugin = { + meta: { + name: "@fedify/lint", + version: "2.0.0", + }, + rules, + configs: { + recommended: { + plugins: ["@fedify/lint"], + rules: recommendedRules, + }, + strict: { + plugins: ["@fedify/lint"], + rules: strictRules, + }, + }, +}; + +export default plugin; + +// Named exports for convenience +export { RULE_IDS, rules }; +export type { TSESLint }; diff --git a/packages/lint/src/index.ts b/packages/lint/src/index.ts index e69de29bb..e7682d85c 100644 --- a/packages/lint/src/index.ts +++ b/packages/lint/src/index.ts @@ -0,0 +1,34 @@ +/** + * @fedify/lint - Fedify linting rules and plugins + * + * This package provides lint rules for both Deno.lint and ESLint. + * + * @example ESLint usage: + * ```js + * // eslint.config.js + * import fedifyLint from "@fedify/lint"; + * + * export default [ + * { + * plugins: { + * "@fedify/lint": fedifyLint, + * }, + * rules: { + * "@fedify/lint/actor-id-required": "warn", + * "@fedify/lint/actor-id-mismatch": "error", + * }, + * }, + * ]; + * ``` + * + * Or use the recommended configuration: + * ```js + * // eslint.config.js + * import fedifyLint from "@fedify/lint"; + * + * export default [ + * fedifyLint.configs.recommended, + * ]; + * ``` + */ +export { default, RULE_IDS, rules } from "./eslint.ts"; diff --git a/packages/lint/src/lib/ast-types.ts b/packages/lint/src/lib/ast-types.ts new file mode 100644 index 000000000..e2eaf7792 --- /dev/null +++ b/packages/lint/src/lib/ast-types.ts @@ -0,0 +1,51 @@ +/** + * Common AST type definitions for ESLint rules. + * Uses TSESTree types from @typescript-eslint/utils. + */ +import type { TSESTree } from "@typescript-eslint/utils"; + +// ============================================================================ +// Re-export TSESTree types with aliases for convenience +// ============================================================================ + +export type Node = TSESTree.Node; +export type Expression = TSESTree.Expression; +export type Statement = TSESTree.Statement; +export type Parameter = TSESTree.Parameter; +export type Pattern = TSESTree.BindingName; + +export type Identifier = TSESTree.Identifier; +export type MemberExpression = TSESTree.MemberExpression; +export type CallExpression = TSESTree.CallExpression; +export type ObjectExpression = TSESTree.ObjectExpression; +export type Property = TSESTree.Property; +export type NewExpression = TSESTree.NewExpression; +export type ConditionalExpression = TSESTree.ConditionalExpression; +export type SpreadElement = TSESTree.SpreadElement; +export type ArrowFunctionExpression = TSESTree.ArrowFunctionExpression; +export type FunctionExpression = TSESTree.FunctionExpression; +export type BlockStatement = TSESTree.BlockStatement; +export type ReturnStatement = TSESTree.ReturnStatement; +export type VariableDeclarator = TSESTree.VariableDeclarator; +export type Program = TSESTree.Program; + +// ============================================================================ +// Custom Union Types +// ============================================================================ + +export type FunctionNode = ArrowFunctionExpression | FunctionExpression; + +// ============================================================================ +// CallExpression with MemberExpression callee +// ============================================================================ + +export interface CallMemberExpression extends Omit { + callee: MemberExpression; +} + +export interface CallMemberExpressionWithIdentifier + extends Omit { + callee: MemberExpression & { + property: Identifier; + }; +} diff --git a/packages/lint/src/lib/common-pred.ts b/packages/lint/src/lib/common-pred.ts new file mode 100644 index 000000000..6dc7eebbc --- /dev/null +++ b/packages/lint/src/lib/common-pred.ts @@ -0,0 +1,131 @@ +/** + * Common predicate functions for AST node checking. + * These functions work with TSESTree AST nodes from @typescript-eslint/utils. + */ +import { pipe, prop } from "@fxts/core"; +import type { TSESTree } from "@typescript-eslint/utils"; +import type { + CallExpression, + CallMemberExpression, + CallMemberExpressionWithIdentifier, + FunctionNode, + Identifier, + MemberExpression, + Node, +} from "./ast-types.ts"; +import { eq } from "./utils.ts"; + +/** + * Combines multiple predicates with AND logic. + */ +export function allOf( + ...predicates: Array<(value: T) => boolean> +): (value: T) => boolean { + return (value: T): boolean => + predicates.every((predicate) => predicate(value)); +} + +/** + * Type guard to check if a value is a valid AST node. + */ +export const isNode = (value: unknown): value is Node => + typeof value === "object" && + value !== null && + "type" in value && + typeof (value as { type: unknown }).type === "string"; + +/** + * Checks if a node is of a specific type. + */ +export const isNodeType = + (type: T) => + (node: Node): node is Extract => node.type === type; + +/** + * Checks if a node has a specific name property (for Identifier nodes). + */ +export const isNodeName = + (name: T) => + (node: Identifier): node is Identifier & { name: T } => node.name === name; + +/** + * Type guard for Identifier node. + */ +export const isIdentifier = (node: Node): node is Identifier => + node.type === "Identifier"; + +/** + * Checks if a node's key is an Identifier (for Property nodes). + */ +export const hasIdentifierKey = ( + node: TSESTree.Property, +): node is TSESTree.Property & { key: Identifier } => + node.key.type === "Identifier"; + +/** + * Checks if a node's callee is a MemberExpression. + */ +export const hasMemberExpressionCallee = ( + node: CallExpression, +): node is CallMemberExpression => node.callee.type === "MemberExpression"; + +/** + * Checks if a node's callee property is an Identifier. + */ +export const hasIdentifierProperty = ( + node: CallMemberExpression, +): node is CallMemberExpressionWithIdentifier => + node.callee.property.type === "Identifier"; + +/** + * Checks if a node's callee property name matches the given method name. + */ +export const hasMethodName = + (methodName: T) => + (node: CallMemberExpressionWithIdentifier): boolean => + node.callee.property.name === methodName; + +/** + * Checks if a CallExpression has minimum required arguments. + */ +export const hasMinArguments = + (min: number) => (node: CallExpression): boolean => + node.arguments.length >= min; + +/** + * Checks if an expression is an arrow function. + */ +export const isArrowFunction = ( + node: Node, +): node is TSESTree.ArrowFunctionExpression => + node.type === "ArrowFunctionExpression"; + +/** + * Checks if an expression is a function expression. + */ +export const isFunctionExpression = ( + node: Node, +): node is TSESTree.FunctionExpression => node.type === "FunctionExpression"; + +/** + * Checks if an expression is a function (arrow or regular). + */ +export const isFunction = (node: Node): node is FunctionNode => + isArrowFunction(node) || isFunctionExpression(node); + +/** + * Checks if a CallExpression is a setActorDispatcher call with proper structure. + */ +export const isSetActorDispatcherCall = ( + node: CallExpression, +): node is CallMemberExpressionWithIdentifier & { + callee: MemberExpression & { + property: Identifier & { name: "setActorDispatcher" }; + }; +} => { + if (!hasMemberExpressionCallee(node)) return false; + if (!hasIdentifierProperty(node)) return false; + if (!hasMethodName("setActorDispatcher")(node)) return false; + if (!hasMinArguments(2)(node)) return false; + return true; +}; diff --git a/packages/lint/src/lib/common-tracker.ts b/packages/lint/src/lib/common-tracker.ts new file mode 100644 index 000000000..67d039953 --- /dev/null +++ b/packages/lint/src/lib/common-tracker.ts @@ -0,0 +1,59 @@ +/** + * Common tracker for Federation variable tracking. + * Works with TSESTree AST nodes from @typescript-eslint/utils. + */ +import type { TSESTree } from "@typescript-eslint/utils"; +import type { CallExpression, Node, VariableDeclarator } from "./ast-types.ts"; + +/** + * Checks if a CallExpression is a createFederation or createFederationBuilder call. + */ +const isCreateFederation = (node: CallExpression): boolean => + node.callee.type === "Identifier" && + /^create(Federation|FederationBuilder)$/i.test(node.callee.name); + +/** + * Helper to track variable names that store the result of createFederation() + * or createFederationBuilder() calls. + */ +export function trackFederationVariables() { + const federationVariables = new Set(); + + const isFederationObject = (obj: TSESTree.Expression): boolean => { + switch (obj.type) { + case "Identifier": + return federationVariables.has(obj.name); + case "CallExpression": { + // Check if it's a direct createFederation call + if (isCreateFederation(obj)) return true; + // Check if it's a chained method call on a federation object + if (obj.callee.type === "MemberExpression") { + return isFederationObject(obj.callee.object); + } + return false; + } + case "MemberExpression": + return isFederationObject(obj.object); + } + return false; + }; + + return { + VariableDeclarator(node: VariableDeclarator) { + const init = node.init; + const id = node.id; + + if (init?.type === "CallExpression") { + if (isCreateFederation(init) && id.type === "Identifier") { + federationVariables.add(id.name); + } + } + }, + + isFederationVariable(name: string): boolean { + return federationVariables.has(name); + }, + + isFederationObject, + }; +} diff --git a/packages/lint/src/lib/eslint-rule-factory.ts b/packages/lint/src/lib/eslint-rule-factory.ts new file mode 100644 index 000000000..52c028fd1 --- /dev/null +++ b/packages/lint/src/lib/eslint-rule-factory.ts @@ -0,0 +1,754 @@ +/** + * ESLint rule factory for creating Fedify lint rules. + * Adapts common rule logic to ESLint's API. + */ +import { + filter, + isArray, + isEmpty, + isNil, + isObject, + negate, + pipe, + pipeLazy, + prop, + some, + toArray, +} from "@fxts/core"; +import type { TSESLint, TSESTree } from "@typescript-eslint/utils"; +import type { ASTNode, CallExpression, FunctionNode } from "./ast-types.ts"; +import { + allOf, + hasIdentifierKey, + hasIdentifierProperty, + hasMemberExpressionCallee, + hasMethodName, + hasMinArguments, + isASTNode, + isFunction, + isNodeName, + isNodeType, + isSetActorDispatcherCall, +} from "./common-pred.ts"; +import { trackFederationVariables } from "./common-tracker.ts"; +import { + actorPropertyMismatch, + actorPropertyRequired, + COLLECTION_FILTERING_NOT_IMPLEMENTED_ERROR, +} from "./messages.ts"; +import type { MethodCallContext, PropertyConfig } from "./types.ts"; +import { eq } from "./utils.ts"; + +// ============================================================================ +// Types +// ============================================================================ + +type ESLintRuleContext = TSESLint.RuleContext; + +interface ActorDispatcherInfo { + node: CallExpression; + dispatcherArg: FunctionNode; +} + +// ============================================================================ +// Property Checkers +// ============================================================================ + +/** + * Checks if a value is a valid AST node object. + */ +const isASTNodeObj = (node: unknown): node is ASTNode => + isObject(node) && !isNil(node); + +/** + * Filters and casts array items to ASTNode. + */ +const filterASTNodes = (items: unknown[]): ASTNode[] => + pipe( + items, + filter(isObject), + filter((p): p is ASTNode => !isNil(p)), + toArray, + ); + +const isIdentifierWithName = + (name: T) => + (node: N): node is N & { type: "Identifier"; name: T } => + allOf(isNodeType("Identifier"), isNodeName(name))(node); + +const isPropertyWithKeyName = (path: string) => +( + node: ASTNode, +): node is { type: "Property"; key: { name: string } & ASTNode } & ASTNode => + allOf( + isNodeType("Property"), + hasIdentifierKey, + pipeLazy( + prop("key")<{ key: { name: string } }>, + prop("name"), + eq(path), + ) as (value: unknown) => boolean, + )(node); + +// ============================================================================ +// Required Rule Property Checker +// ============================================================================ + +/** + * Creates a predicate function that checks if a property has a specific name. + */ +const createPropertyChecker = + (propertyName: T) => (node: unknown): boolean => + isASTNode(node) && isPropertyWithKeyName(propertyName)(node); + +/** + * Internal recursive checker for nested property paths. + */ +const checkNestedPropertyPath = + (path: readonly string[]) => (node: unknown): boolean => { + if (!isASTNode(node) || !isPropertyWithKeyName(path[0])(node)) return false; + if (path.length === 1) return true; + + const value = (node as { value: ASTNode }).value; + + // Handle ObjectExpression: endpoints: { sharedInbox: ... } + if (isNodeType("ObjectExpression")(value)) { + return pipe( + (value as { properties: unknown[] }).properties, + filterASTNodes, + some(checkNestedPropertyPath(path.slice(1))), + ); + } + + // Handle NewExpression: endpoints: new Endpoints({ sharedInbox: ... }) + if (isNodeType("NewExpression")(value)) { + const args = (value as { arguments: unknown[] }).arguments; + if (!isArray(args) || args.length === 0) return false; + const firstArg = args[0]; + if ( + !isASTNodeObj(firstArg) || !isNodeType("ObjectExpression")(firstArg) + ) { + return false; + } + return pipe( + (firstArg as { properties: unknown[] }).properties, + filterASTNodes, + some(checkNestedPropertyPath(path.slice(1))), + ); + } + + return false; + }; + +/** + * Creates a predicate function that checks if a nested property exists. + */ +const createNestedPropertyChecker = + (path: readonly string[]) => (node: unknown): boolean => + checkNestedPropertyPath(path)(node); + +/** + * Checks if an ObjectExpression node contains a property. + */ +const checkObjectExpression = + (propertyChecker: (prop: unknown) => boolean) => (obj: ASTNode): boolean => + pipe( + obj as { properties: unknown[] }, + prop("properties"), + (properties) => + Array.isArray(properties) + ? pipe(properties, filter(isASTNode), some(propertyChecker)) + : false, + ); + +/** + * Extracts the first argument if it's an ObjectExpression. + */ +const extractFirstArgument = (node: ASTNode): ASTNode | null => + pipe( + node as { arguments: unknown[] }, + prop("arguments"), + (args) => { + if (!Array.isArray(args) || args.length === 0) return null; + const firstArg = args[0]; + return isASTNode(firstArg) && isNodeType("ObjectExpression")(firstArg) + ? firstArg + : null; + }, + ); + +/** + * Extracts ObjectExpression from NewExpression. + */ +const extractObjectExpression = (arg: ASTNode): ASTNode | null => { + if (isNodeType("NewExpression")(arg)) return extractFirstArgument(arg); + return null; +}; + +/** + * Checks if a ConditionalExpression has the property in both branches. + */ +const checkConditionalExpression = + (propertyChecker: (prop: unknown) => boolean) => (node: ASTNode): boolean => { + const consequent = (node as { consequent: ASTNode }).consequent; + const alternate = (node as { alternate: ASTNode }).alternate; + + const checkBranch = (branch: ASTNode): boolean => { + if (isNodeType("ConditionalExpression")(branch)) { + return checkConditionalExpression(propertyChecker)(branch); + } + const objExpr = extractObjectExpression(branch); + return objExpr ? checkObjectExpression(propertyChecker)(objExpr) : false; + }; + + return checkBranch(consequent) && checkBranch(alternate); + }; + +/** + * Checks if a ReturnStatement node contains a property. + */ +const checkReturnStatement = + (propertyChecker: (prop: unknown) => boolean) => (node: ASTNode): boolean => { + const arg = (node as { argument: ASTNode | null }).argument; + if (!arg || !isASTNode(arg)) return false; + + if (isNodeType("ConditionalExpression")(arg)) { + return checkConditionalExpression(propertyChecker)(arg); + } + + const objExpr = extractObjectExpression(arg); + return objExpr ? checkObjectExpression(propertyChecker)(objExpr) : false; + }; + +/** + * Creates a function that recursively checks for a property in an AST node. + */ +const createPropertySearcher = + (propertyChecker: (prop: unknown) => boolean) => (node: unknown): boolean => { + if (!isASTNode(node)) return false; + + if (isNodeType("ReturnStatement")(node)) { + return checkReturnStatement(propertyChecker)(node); + } + + if (isNodeType("BlockStatement")(node)) { + return (node as { body: unknown[] }).body.some( + createPropertySearcher(propertyChecker), + ); + } + + // Handle arrow function with direct NewExpression body + if (isNodeType("NewExpression")(node)) { + const objExpr = extractFirstArgument(node); + return objExpr ? checkObjectExpression(propertyChecker)(objExpr) : false; + } + + return false; + }; + +// ============================================================================ +// Mismatch Rule Value Checker +// ============================================================================ + +/** + * Checks if a node is a CallExpression calling the expected context method. + */ +const isExpectedMethodCall = ( + node: ASTNode, + { ctxName, idName, methodName, requiresIdentifier }: MethodCallContext, +): boolean => { + if ( + !isNodeType("CallExpression")(node) || + !hasMemberExpressionCallee(node as unknown as CallExpression) || + !isIdentifierWithName(ctxName)( + ((node as unknown as CallExpression).callee as { object: ASTNode }) + .object, + ) || + !isIdentifierWithName(methodName)( + ((node as unknown as CallExpression).callee as { property: ASTNode }) + .property, + ) + ) return false; + + const args = (node as unknown as CallExpression).arguments; + if (!requiresIdentifier) { + return isEmpty(args); + } + return !isEmpty(args) && some(isIdentifierWithName(idName))(args); +}; + +/** + * Creates a property existence checker for the given property path. + */ +const createPropertyExistenceChecker = (path: readonly string[]) => { + const checkPropertyExists = + (path: readonly string[]) => (node: ASTNode): boolean => { + if (!isPropertyWithKeyName(path[0])(node)) return false; + if (path.length === 1) return true; + + const value = (node as { value: ASTNode }).value; + + if (isNodeType("ObjectExpression")(value)) { + return pipe( + (value as { properties: unknown[] }).properties, + filterASTNodes, + some(checkPropertyExists(path.slice(1))), + ); + } + + if (isNodeType("NewExpression")(value)) { + const args = (value as { arguments: unknown[] }).arguments; + if (!isArray(args) || args.length === 0) return false; + const firstArg = args[0]; + if ( + !isASTNodeObj(firstArg) || !isNodeType("ObjectExpression")(firstArg) + ) { + return false; + } + return pipe( + (firstArg as { properties: unknown[] }).properties, + filterASTNodes, + some(checkPropertyExists(path.slice(1))), + ); + } + + return false; + }; + + return (prop: ASTNode): boolean => + allOf(isASTNodeObj, checkPropertyExists(path))(prop); +}; + +/** + * Creates a property value checker for the given property path. + */ +const createPropertyValueChecker = ( + path: readonly string[], + ctx: MethodCallContext, +) => { + const checkPropertyValue = + (path: readonly string[]) => (prop: ASTNode): boolean => { + if (!isPropertyWithKeyName(path[0])(prop)) return false; + + const value = (prop as { value: ASTNode }).value; + if (path.length === 1) { + return isExpectedMethodCall(value, ctx); + } + + if (isNodeType("ObjectExpression")(value)) { + return pipe( + (value as { properties: unknown[] }).properties, + filterASTNodes, + some(checkPropertyValue(path.slice(1))), + ); + } + + if (isNodeType("NewExpression")(value)) { + const args = (value as { arguments: unknown[] }).arguments; + if (!isArray(args) || args.length === 0) return false; + const firstArg = args[0]; + if ( + !isASTNodeObj(firstArg) || !isNodeType("ObjectExpression")(firstArg) + ) { + return false; + } + return pipe( + (firstArg as { properties: unknown[] }).properties, + filterASTNodes, + some(checkPropertyValue(path.slice(1))), + ); + } + + return false; + }; + + return (prop: ASTNode): boolean => + allOf(isASTNodeObj, checkPropertyValue(path))(prop); +}; + +/** + * Checks if a function body contains the correct property value. + */ +const checkFunctionBody = + (propertyChecker: (prop: ASTNode) => boolean) => (node: ASTNode): boolean => { + if (!isASTNodeObj(node)) return false; + + if (isNodeType("ReturnStatement")(node)) { + const arg = (node as { argument: ASTNode | null }).argument; + if (!arg || !isASTNodeObj(arg)) return false; + + if (isNodeType("ConditionalExpression")(arg)) { + const checkBranch = (branch: ASTNode): boolean => { + if (isNodeType("ConditionalExpression")(branch)) { + return checkFunctionBody(propertyChecker)(branch); + } + if (isNodeType("NewExpression")(branch)) { + return (branch as { arguments: unknown[] }).arguments + .filter(isNodeType("ObjectExpression")) + .some(checkObjectExpression(propertyChecker)); + } + return false; + }; + return ( + checkBranch((arg as { consequent: ASTNode }).consequent) && + checkBranch((arg as { alternate: ASTNode }).alternate) + ); + } + + if (isNodeType("NewExpression")(arg)) { + return (arg as { arguments: unknown[] }).arguments + .filter(isNodeType("ObjectExpression")) + .some(checkObjectExpression(propertyChecker)); + } + + return false; + } + + if ( + isNodeType("BlockStatement")(node) && + Array.isArray((node as { body: unknown[] }).body) + ) { + return (node as { body: ASTNode[] }).body.some( + checkFunctionBody(propertyChecker), + ); + } + + if (isNodeType("NewExpression")(node)) { + return (node as { arguments: unknown[] }).arguments + .filter(isNodeType("ObjectExpression")) + .some(checkObjectExpression(propertyChecker)); + } + + if (isNodeType("ConditionalExpression")(node)) { + const checkBranch = (branch: ASTNode): boolean => { + if (isNodeType("ConditionalExpression")(branch)) { + return checkFunctionBody(propertyChecker)(branch); + } + if (isNodeType("NewExpression")(branch)) { + return (branch as { arguments: unknown[] }).arguments + .filter(isNodeType("ObjectExpression")) + .some(checkObjectExpression(propertyChecker)); + } + return false; + }; + return ( + checkBranch((node as { consequent: ASTNode }).consequent) && + checkBranch((node as { alternate: ASTNode }).alternate) + ); + } + + return false; + }; + +/** + * Extracts parameter names from a function. + */ +const extractParams = ( + fn: FunctionNode, +): [string | null, string | null] => { + const params = (fn as { params: ASTNode[] }).params; + if (params.length < 2) return [null, null]; + + return params.slice(0, 2).map((node) => + isNodeType("Identifier")(node) ? (node as { name: string }).name : null + ) as [string | null, string | null]; +}; + +// ============================================================================ +// Dispatcher Tracker +// ============================================================================ + +/** + * Tracks dispatcher method calls on federation objects. + */ +const createDispatcherTracker = ( + dispatcherMethod: string, + federationTracker: ReturnType, +) => { + let dispatcherConfigured = false; + + const isDispatcherMethodCall = (node: CallExpression): boolean => + allOf( + hasMemberExpressionCallee, + hasIdentifierProperty, + hasMethodName(dispatcherMethod), + )( + node as unknown as { + callee: { property: { name: string }; object: ASTNode }; + } & CallExpression, + ); + + return { + isDispatcherConfigured: () => dispatcherConfigured, + checkDispatcherCall: (node: CallExpression) => { + if ( + isDispatcherMethodCall(node) && + federationTracker.isFederationObject( + (node.callee as { object: ASTNode }).object, + ) + ) { + dispatcherConfigured = true; + } + }, + }; +}; + +// ============================================================================ +// ESLint Rule Factory: Required Rules +// ============================================================================ + +/** + * Creates an ESLint rule that checks if a property is required. + */ +export function createESLintRequiredRule( + ruleId: string, + config: PropertyConfig, +): TSESLint.RuleModule { + const propertyChecker = config.path.length === 1 + ? createPropertyChecker(config.path[0]) + : createNestedPropertyChecker(config.path); + const propertySearcher = createPropertySearcher(propertyChecker); + + return { + meta: { + type: "suggestion", + docs: { + description: `Ensure actor dispatcher returns ${ + config.path.join(".") + } property`, + }, + schema: [], + messages: { + required: "{{ message }}", + }, + }, + defaultOptions: [], + create(context: ESLintRuleContext) { + const federationTracker = trackFederationVariables(); + const dispatcherTracker = createDispatcherTracker( + config.setter, + federationTracker, + ); + const actorDispatchers: ActorDispatcherInfo[] = []; + + return { + VariableDeclarator(node: TSESTree.VariableDeclarator) { + federationTracker.VariableDeclarator( + node as unknown as ASTNode & { + id: ASTNode; + init: ASTNode | null; + }, + ); + }, + + CallExpression(node: TSESTree.CallExpression) { + const callNode = node as unknown as CallExpression; + dispatcherTracker.checkDispatcherCall(callNode); + + if (!isSetActorDispatcherCall(callNode)) return; + if ( + !federationTracker.isFederationObject( + (callNode.callee as { object: ASTNode }).object, + ) + ) return; + + const dispatcherArg = callNode.arguments[1] as unknown as ASTNode; + if (isFunction(dispatcherArg)) { + actorDispatchers.push({ + node: callNode, + dispatcherArg: dispatcherArg as FunctionNode, + }); + } + }, + + "Program:exit"() { + if (!dispatcherTracker.isDispatcherConfigured()) return; + + for (const { dispatcherArg } of actorDispatchers) { + const body = (dispatcherArg as { body: ASTNode }).body; + if (!propertySearcher(body)) { + context.report({ + node: dispatcherArg as unknown as TSESTree.Node, + messageId: "required", + data: { message: actorPropertyRequired(config) }, + }); + } + } + }, + }; + }, + }; +} + +// ============================================================================ +// ESLint Rule Factory: Mismatch Rules +// ============================================================================ + +/** + * Creates an ESLint rule that checks if a property uses the correct context method. + */ +export function createESLintMismatchRule( + ruleId: string, + config: PropertyConfig, +): TSESLint.RuleModule { + return { + meta: { + type: "problem", + docs: { + description: `Ensure actor's ${ + config.path.join(".") + } property uses correct context method`, + }, + schema: [], + messages: { + mismatch: "{{ message }}", + }, + }, + defaultOptions: [], + create(context: ESLintRuleContext) { + const tracker = trackFederationVariables(); + + return { + VariableDeclarator(node: TSESTree.VariableDeclarator) { + tracker.VariableDeclarator( + node as unknown as ASTNode & { + id: ASTNode; + init: ASTNode | null; + }, + ); + }, + + CallExpression(node: TSESTree.CallExpression) { + const callNode = node as unknown as CallExpression; + + if ( + !isSetActorDispatcherCall(callNode) || + !hasMemberExpressionCallee(callNode) || + !tracker.isFederationObject( + (callNode.callee as { object: ASTNode }).object, + ) + ) return; + + const dispatcherArg = callNode.arguments[1] as unknown as ASTNode; + if (!isFunction(dispatcherArg)) return; + + const [ctxName, idName] = extractParams( + dispatcherArg as FunctionNode, + ); + if (!ctxName || !idName) return; + + const methodCallContext: MethodCallContext = { + path: config.path.join("."), + ctxName, + idName, + methodName: config.getter, + requiresIdentifier: config.requiresIdentifier, + }; + + const body = (dispatcherArg as { body: ASTNode }).body; + const existenceChecker = createPropertyExistenceChecker(config.path); + const hasProperty = checkFunctionBody(existenceChecker)(body); + + if (!hasProperty) return; + + const valueChecker = createPropertyValueChecker( + config.path, + methodCallContext, + ); + const hasCorrectValue = checkFunctionBody(valueChecker)(body); + + if (!hasCorrectValue) { + context.report({ + node: dispatcherArg as unknown as TSESTree.Node, + messageId: "mismatch", + data: { message: actorPropertyMismatch(methodCallContext) }, + }); + } + }, + }; + }, + }; +} + +// ============================================================================ +// ESLint Rule Factory: Collection Filtering +// ============================================================================ + +const FOLLOWERS_DISPATCHER_METHOD = "setFollowersDispatcher" as const; + +/** + * Checks if a node is a setFollowersDispatcher call. + */ +const isFollowersDispatcherCall = (node: CallExpression): boolean => + allOf( + hasMemberExpressionCallee, + hasIdentifierProperty, + hasMinArguments(2), + hasMethodName(FOLLOWERS_DISPATCHER_METHOD), + )( + node as unknown as { + callee: { property: { name: string }; object: ASTNode }; + } & CallExpression, + ); + +/** + * Checks if a function node has the filter parameter (4th parameter). + */ +const hasFilterParameter = (fn: FunctionNode): boolean => + (fn as { params: unknown[] }).params.length >= 4; + +/** + * Creates the collection-filtering-not-implemented ESLint rule. + */ +export function createESLintCollectionFilteringRule( + ruleId: string, +): TSESLint.RuleModule { + return { + meta: { + type: "suggestion", + docs: { + description: "Ensure followers dispatcher implements filtering", + }, + schema: [], + messages: { + filterRequired: COLLECTION_FILTERING_NOT_IMPLEMENTED_ERROR, + }, + }, + defaultOptions: [], + create(context: ESLintRuleContext) { + const federationTracker = trackFederationVariables(); + + return { + VariableDeclarator(node: TSESTree.VariableDeclarator) { + federationTracker.VariableDeclarator( + node as unknown as ASTNode & { + id: ASTNode; + init: ASTNode | null; + }, + ); + }, + + CallExpression(node: TSESTree.CallExpression) { + const callNode = node as unknown as CallExpression; + + if (!isFollowersDispatcherCall(callNode)) return; + if ( + !federationTracker.isFederationObject( + (callNode.callee as { object: ASTNode }).object, + ) + ) return; + + const dispatcherArg = callNode.arguments[1] as unknown as ASTNode; + if (!isFunction(dispatcherArg)) return; + + if (!hasFilterParameter(dispatcherArg as FunctionNode)) { + context.report({ + node: dispatcherArg as unknown as TSESTree.Node, + messageId: "filterRequired", + }); + } + }, + }; + }, + }; +} diff --git a/packages/lint/src/lib/eslint-types.ts b/packages/lint/src/lib/eslint-types.ts new file mode 100644 index 000000000..eab4c7961 --- /dev/null +++ b/packages/lint/src/lib/eslint-types.ts @@ -0,0 +1,141 @@ +/** + * ESLint adapter types and utilities. + * Provides compatibility layer between ESLint and our common AST types. + */ +import type { TSESLint, TSESTree } from "@typescript-eslint/utils"; +import type { + ASTNode, + CallExpression, + FunctionNode, + VariableDeclarator, +} from "./ast-types.ts"; +import type { PropertyConfig } from "./types.ts"; + +// ============================================================================ +// ESLint Rule Types +// ============================================================================ + +/** + * ESLint rule context type + */ +export type ESLintRuleContext = TSESLint.RuleContext; + +/** + * ESLint rule module type + */ +export type ESLintRuleModule = TSESLint.RuleModule; + +/** + * ESLint rule listener type + */ +export type ESLintRuleListener = TSESLint.RuleListener; + +/** + * ESLint fixer type + */ +export type ESLintFixer = TSESLint.RuleFixer; + +// ============================================================================ +// Report Descriptor Types +// ============================================================================ + +export interface ReportDescriptor { + node: ASTNode; + message: string; +} + +// ============================================================================ +// Unified Rule Context Interface +// ============================================================================ + +/** + * Unified context interface for both Deno.lint and ESLint. + */ +export interface UnifiedRuleContext { + report(descriptor: ReportDescriptor): void; +} + +/** + * Creates a unified context from an ESLint rule context. + */ +export const createUnifiedContext = ( + eslintContext: ESLintRuleContext, + messageId: string, +): UnifiedRuleContext => ({ + report: ({ node, message }) => { + eslintContext.report({ + node: node as unknown as TSESTree.Node, + messageId, + data: { message }, + }); + }, +}); + +// ============================================================================ +// Unified Rule Visitor Interface +// ============================================================================ + +export interface UnifiedRuleVisitor { + VariableDeclarator?(node: VariableDeclarator): void; + CallExpression?(node: CallExpression): void; + "Program:exit"?(): void; +} + +// ============================================================================ +// Rule Factory Types +// ============================================================================ + +/** + * Actor dispatcher info for tracking + */ +export interface ActorDispatcherInfo { + node: CallExpression; + dispatcherArg: FunctionNode; +} + +/** + * Factory function type for creating rules + */ +export type RuleFactory = (config: PropertyConfig) => { + create(context: UnifiedRuleContext): UnifiedRuleVisitor; +}; + +// ============================================================================ +// ESLint Plugin Types +// ============================================================================ + +/** + * ESLint plugin configuration + */ +export interface ESLintPlugin { + meta: { + name: string; + version: string; + }; + rules: Record; + configs: Record; +} + +/** + * ESLint plugin config + */ +export interface ESLintPluginConfig { + plugins?: string[]; + rules?: Record; +} + +// ============================================================================ +// Node Type Guards (ESLint-specific) +// ============================================================================ + +/** + * Converts TSESTree node to common ASTNode type. + */ +export const toASTNode = (node: TSESTree.Node): ASTNode => + node as unknown as ASTNode; + +/** + * Converts common ASTNode to TSESTree node type. + */ +export const toTSESTreeNode = (node: ASTNode): TSESTree.Node => + node as unknown as TSESTree.Node; diff --git a/packages/lint/src/lib/property-checker-eslint.ts b/packages/lint/src/lib/property-checker-eslint.ts new file mode 100644 index 000000000..57cd1e47d --- /dev/null +++ b/packages/lint/src/lib/property-checker-eslint.ts @@ -0,0 +1,283 @@ +/** + * Property checkers for ESLint rules. + * Uses TSESTree types from @typescript-eslint/utils. + */ +import { pipe, some } from "@fxts/core"; +import type { TSESTree } from "@typescript-eslint/utils"; +import type { MethodCallContext } from "./types.ts"; + +// ============================================================================ +// Type Aliases +// ============================================================================ + +type Node = TSESTree.Node; +type Property = TSESTree.Property; +type ObjectExpression = TSESTree.ObjectExpression; +type NewExpression = TSESTree.NewExpression; +type BlockStatement = TSESTree.BlockStatement; +type ReturnStatement = TSESTree.ReturnStatement; +type ConditionalExpression = TSESTree.ConditionalExpression; +type CallExpression = TSESTree.CallExpression; +type MemberExpression = TSESTree.MemberExpression; +type Identifier = TSESTree.Identifier; +type FunctionNode = + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionExpression; + +// ============================================================================ +// Basic Type Guards +// ============================================================================ + +const isProperty = (node: Node): node is Property => node.type === "Property"; + +const isObjectExpression = (node: Node): node is ObjectExpression => + node.type === "ObjectExpression"; + +const isNewExpression = (node: Node): node is NewExpression => + node.type === "NewExpression"; + +const isBlockStatement = (node: Node): node is BlockStatement => + node.type === "BlockStatement"; + +const isReturnStatement = (node: Node): node is ReturnStatement => + node.type === "ReturnStatement"; + +const isConditionalExpression = (node: Node): node is ConditionalExpression => + node.type === "ConditionalExpression"; + +const isCallExpression = (node: Node): node is CallExpression => + node.type === "CallExpression"; + +const isIdentifier = (node: Node): node is Identifier => + node.type === "Identifier"; + +const isMemberExpression = (node: Node): node is MemberExpression => + node.type === "MemberExpression"; + +// ============================================================================ +// Property Name Checkers +// ============================================================================ + +/** + * Checks if a Property node has an Identifier key with a specific name. + */ +const hasPropertyKeyName = (name: string) => (node: Property): boolean => + node.key.type === "Identifier" && node.key.name === name; + +/** + * Finds a property with a specific name in an ObjectExpression. + */ +const findPropertyByName = + (name: string) => (obj: ObjectExpression): Property | undefined => + obj.properties.find( + (p): p is Property => isProperty(p) && hasPropertyKeyName(name)(p), + ); + +// ============================================================================ +// ObjectExpression Extractors +// ============================================================================ + +/** + * Extracts the first ObjectExpression argument from a NewExpression. + */ +const extractObjectFromNewExpression = ( + node: NewExpression, +): ObjectExpression | null => { + const firstArg = node.arguments[0]; + if (firstArg && isObjectExpression(firstArg)) { + return firstArg; + } + return null; +}; + +/** + * Extracts ObjectExpression from a Property value. + * Handles both direct ObjectExpression and NewExpression with ObjectExpression argument. + */ +const extractObjectFromPropertyValue = ( + prop: Property, +): ObjectExpression | null => { + const value = prop.value; + if (isObjectExpression(value)) { + return value; + } + if (isNewExpression(value)) { + return extractObjectFromNewExpression(value); + } + return null; +}; + +// ============================================================================ +// Property Existence Checker +// ============================================================================ + +/** + * Recursively checks if a property path exists in an ObjectExpression. + */ +const checkPropertyPathExists = + (path: readonly string[]) => (obj: ObjectExpression): boolean => { + if (path.length === 0) return true; + + const prop = findPropertyByName(path[0])(obj); + if (!prop) return false; + + if (path.length === 1) return true; + + const nestedObj = extractObjectFromPropertyValue(prop); + if (!nestedObj) return false; + + return checkPropertyPathExists(path.slice(1))(nestedObj); + }; + +/** + * Creates a checker that verifies a property path exists. + */ +export const createPropertyExistenceChecker = + (path: readonly string[]) => (obj: ObjectExpression): boolean => + checkPropertyPathExists(path)(obj); + +// ============================================================================ +// Property Value Checker +// ============================================================================ + +/** + * Checks if a node is the expected method call. + * e.g., ctx.getActorUri(identifier) + */ +const isExpectedMethodCall = ( + node: Node, + ctx: MethodCallContext, +): boolean => { + if (!isCallExpression(node)) return false; + if (!isMemberExpression(node.callee)) return false; + + const { object, property } = node.callee; + if (!isIdentifier(object) || object.name !== ctx.ctxName) return false; + if (!isIdentifier(property) || property.name !== ctx.methodName) return false; + + if (!ctx.requiresIdentifier) { + return node.arguments.length === 0; + } + + return node.arguments.some( + (arg) => isIdentifier(arg) && arg.name === ctx.idName, + ); +}; + +/** + * Recursively checks if a property path has the correct method call value. + */ +const checkPropertyPathValue = + (path: readonly string[], ctx: MethodCallContext) => + (obj: ObjectExpression): boolean => { + if (path.length === 0) return false; + + const prop = findPropertyByName(path[0])(obj); + if (!prop) return false; + + if (path.length === 1) { + return isExpectedMethodCall(prop.value, ctx); + } + + const nestedObj = extractObjectFromPropertyValue(prop); + if (!nestedObj) return false; + + return checkPropertyPathValue(path.slice(1), ctx)(nestedObj); + }; + +/** + * Creates a checker that verifies a property has the correct method call value. + */ +export const createPropertyValueChecker = + (path: readonly string[], ctx: MethodCallContext) => + (obj: ObjectExpression): boolean => checkPropertyPathValue(path, ctx)(obj); + +// ============================================================================ +// Function Body Checkers +// ============================================================================ + +/** + * Extracts ObjectExpression from a node that might be a NewExpression. + */ +const extractObjectFromNode = (node: Node): ObjectExpression | null => { + if (isNewExpression(node)) { + return extractObjectFromNewExpression(node); + } + return null; +}; + +/** + * Checks a conditional expression branch for a property. + */ +const checkConditionalBranch = + (checker: (obj: ObjectExpression) => boolean) => (node: Node): boolean => { + if (isConditionalExpression(node)) { + return ( + checkConditionalBranch(checker)(node.consequent) && + checkConditionalBranch(checker)(node.alternate) + ); + } + + const obj = extractObjectFromNode(node); + return obj ? checker(obj) : false; + }; + +/** + * Searches a function body for ObjectExpressions and applies a checker. + */ +export const searchFunctionBody = + (checker: (obj: ObjectExpression) => boolean) => (body: Node): boolean => { + if (isBlockStatement(body)) { + return body.body.some(searchFunctionBody(checker)); + } + + if (isReturnStatement(body)) { + const arg = body.argument; + if (!arg) return false; + + if (isConditionalExpression(arg)) { + return checkConditionalBranch(checker)(arg); + } + + const obj = extractObjectFromNode(arg); + return obj ? checker(obj) : false; + } + + // Arrow function with direct expression body + if (isNewExpression(body)) { + const obj = extractObjectFromNewExpression(body); + return obj ? checker(obj) : false; + } + + if (isConditionalExpression(body)) { + return checkConditionalBranch(checker)(body); + } + + return false; + }; + +// ============================================================================ +// Parameter Extraction +// ============================================================================ + +/** + * Extracts the first two parameter names from a function. + * Returns [ctxName, idName] or [null, null] if not enough parameters. + */ +export const extractFunctionParams = ( + fn: FunctionNode, +): [string | null, string | null] => { + const params = fn.params; + if (params.length < 2) return [null, null]; + + const getName = (param: TSESTree.Parameter): string | null => + param.type === "Identifier" ? param.name : null; + + return [getName(params[0]), getName(params[1])]; +}; + +/** + * Checks if a function has at least n parameters. + */ +export const hasMinParams = (min: number) => (fn: FunctionNode): boolean => + fn.params.length >= min; diff --git a/packages/lint/tsdown.config.ts b/packages/lint/tsdown.config.ts index 70a57795c..81633e88a 100644 --- a/packages/lint/tsdown.config.ts +++ b/packages/lint/tsdown.config.ts @@ -1,8 +1,9 @@ import { defineConfig } from "tsdown"; export default defineConfig({ - entry: ["src/index.ts"], + entry: ["src/index.ts", "src/eslint.ts"], dts: true, format: ["esm", "cjs"], platform: "node", + exports: "named", }); From 02b538643d68f642d75f376062e5e79159acb587 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Tue, 9 Dec 2025 05:47:28 +0000 Subject: [PATCH 23/41] Added test for eslint --- packages/lint/package.json | 3 +- packages/lint/src/eslint-rules.ts | 8 +- packages/lint/src/lib/test-eslint.ts | 89 ++++ .../lint/src/lib/test-templates-eslint.ts | 382 ++++++++++++++++++ .../actor-assertion-method-required.test.ts | 20 + .../actor-featured-property-mismatch.test.ts | 20 + .../actor-featured-property-required.test.ts | 20 + ...or-featured-tags-property-mismatch.test.ts | 20 + ...or-featured-tags-property-required.test.ts | 20 + .../actor-followers-property-mismatch.test.ts | 20 + .../actor-followers-property-required.test.ts | 20 + .../actor-following-property-mismatch.test.ts | 20 + .../actor-following-property-required.test.ts | 20 + .../tests/eslint/actor-id-mismatch.test.ts | 20 + .../tests/eslint/actor-id-required.test.ts | 20 + .../actor-inbox-property-mismatch.test.ts | 20 + .../actor-inbox-property-required.test.ts | 20 + .../actor-liked-property-mismatch.test.ts | 20 + .../actor-liked-property-required.test.ts | 20 + .../actor-outbox-property-mismatch.test.ts | 20 + .../actor-outbox-property-required.test.ts | 20 + .../eslint/actor-public-key-required.test.ts | 20 + ...tor-shared-inbox-property-mismatch.test.ts | 20 + ...tor-shared-inbox-property-required.test.ts | 20 + ...llection-filtering-not-implemented.test.ts | 20 + 25 files changed, 897 insertions(+), 5 deletions(-) create mode 100644 packages/lint/src/lib/test-eslint.ts create mode 100644 packages/lint/src/lib/test-templates-eslint.ts create mode 100644 packages/lint/src/tests/eslint/actor-assertion-method-required.test.ts create mode 100644 packages/lint/src/tests/eslint/actor-featured-property-mismatch.test.ts create mode 100644 packages/lint/src/tests/eslint/actor-featured-property-required.test.ts create mode 100644 packages/lint/src/tests/eslint/actor-featured-tags-property-mismatch.test.ts create mode 100644 packages/lint/src/tests/eslint/actor-featured-tags-property-required.test.ts create mode 100644 packages/lint/src/tests/eslint/actor-followers-property-mismatch.test.ts create mode 100644 packages/lint/src/tests/eslint/actor-followers-property-required.test.ts create mode 100644 packages/lint/src/tests/eslint/actor-following-property-mismatch.test.ts create mode 100644 packages/lint/src/tests/eslint/actor-following-property-required.test.ts create mode 100644 packages/lint/src/tests/eslint/actor-id-mismatch.test.ts create mode 100644 packages/lint/src/tests/eslint/actor-id-required.test.ts create mode 100644 packages/lint/src/tests/eslint/actor-inbox-property-mismatch.test.ts create mode 100644 packages/lint/src/tests/eslint/actor-inbox-property-required.test.ts create mode 100644 packages/lint/src/tests/eslint/actor-liked-property-mismatch.test.ts create mode 100644 packages/lint/src/tests/eslint/actor-liked-property-required.test.ts create mode 100644 packages/lint/src/tests/eslint/actor-outbox-property-mismatch.test.ts create mode 100644 packages/lint/src/tests/eslint/actor-outbox-property-required.test.ts create mode 100644 packages/lint/src/tests/eslint/actor-public-key-required.test.ts create mode 100644 packages/lint/src/tests/eslint/actor-shared-inbox-property-mismatch.test.ts create mode 100644 packages/lint/src/tests/eslint/actor-shared-inbox-property-required.test.ts create mode 100644 packages/lint/src/tests/eslint/collection-filtering-not-implemented.test.ts diff --git a/packages/lint/package.json b/packages/lint/package.json index c69e3f92e..0f6a51258 100644 --- a/packages/lint/package.json +++ b/packages/lint/package.json @@ -63,6 +63,7 @@ }, "devDependencies": { "@types/eslint": "^9.0.0", + "@typescript-eslint/rule-tester": "^8.49.0", "tsdown": "catalog:", "typescript": "catalog:" }, @@ -70,6 +71,6 @@ "build": "tsdown", "prepack": "tsdown", "prepublish": "tsdown", - "test": "deno task codegen && tsdown && cd dist/ && node --test" + "test": "tsdown && node --experimental-transform-types --test src/tests/eslint/*.test.ts" } } diff --git a/packages/lint/src/eslint-rules.ts b/packages/lint/src/eslint-rules.ts index ff1d74b17..f2cc0f46f 100644 --- a/packages/lint/src/eslint-rules.ts +++ b/packages/lint/src/eslint-rules.ts @@ -36,6 +36,10 @@ type FunctionNode = // Federation Variable Tracker // ============================================================================ +const isCreateFederationCall = (node: CallExpression): boolean => + node.callee.type === "Identifier" && + /^create(Federation|FederationBuilder)$/i.test(node.callee.name); + interface FederationTracker { handleVariableDeclarator(node: VariableDeclarator): void; isFederationObject(node: Expression): boolean; @@ -44,10 +48,6 @@ interface FederationTracker { function createFederationTracker(): FederationTracker { const federationVariables = new Set(); - const isCreateFederationCall = (node: CallExpression): boolean => - node.callee.type === "Identifier" && - /^create(Federation|FederationBuilder)$/i.test(node.callee.name); - const isFederationObject = (node: Expression): boolean => { switch (node.type) { case "Identifier": diff --git a/packages/lint/src/lib/test-eslint.ts b/packages/lint/src/lib/test-eslint.ts new file mode 100644 index 000000000..c6d406f7b --- /dev/null +++ b/packages/lint/src/lib/test-eslint.ts @@ -0,0 +1,89 @@ +/** + * ESLint test utilities. + * Uses @typescript-eslint/rule-tester for testing ESLint rules. + */ +import { RuleTester } from "@typescript-eslint/rule-tester"; +import type { TSESLint } from "@typescript-eslint/utils"; +import { FEDERATION_SETUP } from "./const.ts"; + +// Configure RuleTester to use node:test +RuleTester.afterAll = () => {}; +RuleTester.it = (title, fn) => fn(); +RuleTester.describe = (_, fn) => fn(); + +export type RuleModule = TSESLint.RuleModule; + +export interface ESLintTestCase { + /** Test case name */ + name: string; + /** Code to test */ + code: string; +} + +export interface ESLintInvalidTestCase extends ESLintTestCase { + /** Expected error message IDs */ + errors: Array<{ messageId: string }>; +} + +/** + * Creates a RuleTester instance for testing ESLint rules. + */ +export function createRuleTester(): RuleTester { + return new RuleTester(); +} + +/** + * Wraps code with federation setup for testing. + */ +export function wrapWithFederationSetup( + code: string, + federationSetup: string | false = FEDERATION_SETUP, +): string { + if (federationSetup === false) return code; + return `${federationSetup}\n\n${code}`; +} + +/** + * Creates valid test cases with federation setup. + */ +export function validCase( + name: string, + code: string, + federationSetup: string | false = FEDERATION_SETUP, +): ESLintTestCase { + return { + name, + code: wrapWithFederationSetup(code, federationSetup), + }; +} + +/** + * Creates invalid test cases with federation setup and expected errors. + */ +export function invalidCase( + name: string, + code: string, + messageId: string, + federationSetup: string | false = FEDERATION_SETUP, +): ESLintInvalidTestCase { + return { + name, + code: wrapWithFederationSetup(code, federationSetup), + errors: [{ messageId }], + }; +} + +/** + * Runs ESLint rule tests. + */ +export function runESLintTests( + ruleName: string, + rule: RuleModule, + tests: { + valid: ESLintTestCase[]; + invalid: ESLintInvalidTestCase[]; + }, +): void { + const tester = createRuleTester(); + tester.run(ruleName, rule, tests); +} diff --git a/packages/lint/src/lib/test-templates-eslint.ts b/packages/lint/src/lib/test-templates-eslint.ts new file mode 100644 index 000000000..34d44399c --- /dev/null +++ b/packages/lint/src/lib/test-templates-eslint.ts @@ -0,0 +1,382 @@ +/** + * ESLint test templates for generating test cases. + * Reuses patterns from Deno lint test templates. + */ +import { properties, type PropertyConfig } from "./const.ts"; +import type { ESLintInvalidTestCase, ESLintTestCase } from "./test-eslint.ts"; +import { invalidCase, validCase } from "./test-eslint.ts"; + +// ============================================================================= +// Types +// ============================================================================= + +type PropertyKey = keyof typeof properties; + +// ============================================================================= +// Common Code Snippets +// ============================================================================= + +const createDispatcherCode = (content: string): string => ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + ${content} + }); +`; + +const createChainedDispatcherCode = ( + content: string, + dispatcherMethod: string, +): string => ` + federation + .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + ${content} + }) + .${dispatcherMethod}(async (ctx, identifier) => []); +`; + +const createSeparateDispatcherCode = ( + content: string, + dispatcherMethod: string, + isBefore: boolean, +): string => { + const dispatcher = + `federation.${dispatcherMethod}(async (ctx, identifier) => []);`; + const actor = createDispatcherCode(content); + return isBefore ? `${dispatcher}\n${actor}` : `${actor}\n${dispatcher}`; +}; + +// ============================================================================= +// Property Code Generation Utilities +// ============================================================================= + +const createMethodCall = ( + getter: string, + requiresIdentifier: boolean, + ctxName = "ctx", + idName = "identifier", +): string => { + return requiresIdentifier + ? `${ctxName}.${getter}(${idName})` + : `${ctxName}.${getter}()`; +}; + +const createPropertyAssignment = ( + prop: PropertyConfig, + ctxName = "ctx", + idName = "identifier", +): string => { + const methodCall = createMethodCall( + prop.getter, + prop.requiresIdentifier, + ctxName, + idName, + ); + + if (prop.path.length === 1) { + return `${prop.path[0]}: ${methodCall},`; + } + + // Handle nested properties like endpoints.sharedInbox + if (prop.path.length === 2 && prop.path[0] === "endpoints") { + return `endpoints: new Endpoints({ ${prop.path[1]}: ${methodCall} }),`; + } + + return `${prop.path[prop.path.length - 1]}: ${methodCall},`; +}; + +const createReturnStatement = (properties: string): string => + `return new Person({ ${properties} });`; + +// ============================================================================= +// Required Rule Test Cases +// ============================================================================= + +export function createRequiredValidCases( + propKey: PropertyKey, +): ESLintTestCase[] { + const prop = properties[propKey]; + const propertyCode = createPropertyAssignment(prop); + + return [ + // Non-federation object + validCase( + "setActorDispatcher called on non-Federation object", + ` + federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return new Person({ name: "John Doe" }); + }); + `, + `const federation = { setActorDispatcher: () => {} };`, + ), + + // With property using method + validCase( + `with ${prop.path.join(".")} property using ctx.${prop.getter}()`, + createDispatcherCode( + createReturnStatement(`name: "John Doe", ${propertyCode}`), + ), + ), + + // With property using hardcoded value (for required rules, any value is fine) + validCase( + `with ${prop.path.join(".")} property (any value)`, + createDispatcherCode( + createReturnStatement( + `name: "John Doe", ${prop.path[prop.path.length - 1]}: "https://example.com/value"`, + ), + ), + ), + + // BlockStatement with property + validCase( + `BlockStatement with ${prop.path.join(".")}`, + createDispatcherCode(` + const name = "John Doe"; + ${createReturnStatement(`name, ${propertyCode}`)} + `), + ), + ]; +} + +export function createRequiredInvalidCases( + propKey: PropertyKey, + messageId: string, +): ESLintInvalidTestCase[] { + const prop = properties[propKey]; + + // For properties with setter !== setActorDispatcher, we need to add the + // dispatcher call to trigger the rule + if (prop.setter !== "setActorDispatcher") { + const dispatcherMethod = + prop.setter === "setInboxListeners" ? "setInboxListeners" : prop.setter; + + return [ + // Chained dispatcher case - this triggers the rule + invalidCase( + `chained ${dispatcherMethod} without ${prop.path.join(".")}`, + createChainedDispatcherCode( + createReturnStatement(`name: "John Doe"`), + dispatcherMethod, + ), + messageId, + ), + + // Separate dispatcher before + invalidCase( + `separate ${dispatcherMethod} before without ${prop.path.join(".")}`, + createSeparateDispatcherCode( + createReturnStatement(`name: "John Doe"`), + dispatcherMethod, + true, + ), + messageId, + ), + + // Separate dispatcher after + invalidCase( + `separate ${dispatcherMethod} after without ${prop.path.join(".")}`, + createSeparateDispatcherCode( + createReturnStatement(`name: "John Doe"`), + dispatcherMethod, + false, + ), + messageId, + ), + ]; + } + + // For id property (setter === setActorDispatcher), use basic cases + return [ + // Without property + invalidCase( + `without ${prop.path.join(".")} property`, + createDispatcherCode(createReturnStatement(`name: "John Doe"`)), + messageId, + ), + + // Empty object + invalidCase( + "returning empty object", + createDispatcherCode(createReturnStatement("")), + messageId, + ), + ]; +} + +// ============================================================================= +// Mismatch Rule Test Cases +// ============================================================================= + +export function createMismatchValidCases( + propKey: PropertyKey, +): ESLintTestCase[] { + const prop = properties[propKey]; + const propertyCode = createPropertyAssignment(prop); + + // Helper to create wrong property assignment for valid cases + const createWrongPropertyCode = (value: string): string => { + if (prop.path.length === 1) { + return `${prop.path[0]}: ${value}`; + } + // Handle nested properties like endpoints.sharedInbox + return `endpoints: new Endpoints({ ${prop.path[1]}: ${value} })`; + }; + + return [ + // Non-federation object + validCase( + "setActorDispatcher called on non-Federation object", + createDispatcherCode( + createReturnStatement( + `name: "John Doe", ${createWrongPropertyCode('"https://example.com/wrong"')}`, + ), + ), + `const federation = { setActorDispatcher: () => {} };`, + ), + + // Correct method call + validCase( + `${prop.path.join(".")} uses ctx.${prop.getter}()`, + createDispatcherCode( + createReturnStatement(`name: "John Doe", ${propertyCode}`), + ), + ), + + // BlockStatement with correct method + validCase( + `BlockStatement with correct ${prop.path.join(".")}`, + createDispatcherCode(` + const name = "John Doe"; + ${createReturnStatement(`name, ${propertyCode}`)} + `), + ), + + // No property at all (let required rule handle this) + validCase( + `no ${prop.path.join(".")} property (required rule handles this)`, + createDispatcherCode(createReturnStatement(`name: "John Doe"`)), + ), + ]; +} + +export function createMismatchInvalidCases( + propKey: PropertyKey, + messageId: string, +): ESLintInvalidTestCase[] { + const prop = properties[propKey]; + + // Helper to create wrong property assignment + const createWrongPropertyAssignment = (value: string): string => { + if (prop.path.length === 1) { + return `${prop.path[0]}: ${value}`; + } + // Handle nested properties like endpoints.sharedInbox + return `endpoints: new Endpoints({ ${prop.path[1]}: ${value} })`; + }; + + return [ + // Hardcoded string + invalidCase( + `${prop.path.join(".")} uses hardcoded string`, + createDispatcherCode( + createReturnStatement( + `name: "John Doe", ${createWrongPropertyAssignment('"https://example.com/wrong"')}`, + ), + ), + messageId, + ), + + // Wrong method + invalidCase( + `${prop.path.join(".")} uses wrong method`, + createDispatcherCode( + createReturnStatement( + `name: "John Doe", ${createWrongPropertyAssignment("ctx.getWrongMethod()")}`, + ), + ), + messageId, + ), + ]; +} + +// ============================================================================= +// Collection Filtering Test Cases +// ============================================================================= + +const createFollowersDispatcherCode = ( + { + params = ["ctx", "identifier", "cursor", "filter"], + async: isAsync = true, + arrow = true, + }: { + params?: readonly string[]; + async?: boolean; + arrow?: boolean; + } = {}, +): string => { + const paramsString = params.join(", "); + const asyncKeyword = isAsync ? "async" : ""; + const [funcKeyword, arrowSymbol] = arrow ? ["", "=>"] : ["function", ""]; + + return ` + federation.setFollowersDispatcher( + "/users/{identifier}/followers", + ${asyncKeyword} ${funcKeyword}(${paramsString}) ${arrowSymbol} { + return { items: [] }; + } + ); + `; +}; + +const filterless = ["ctx", "identifier", "cursor"] as const; + +export function createCollectionFilteringValidCases(): ESLintTestCase[] { + return [ + validCase( + "async arrow function with filter parameter", + createFollowersDispatcherCode(), + ), + validCase( + "async function expression with filter", + createFollowersDispatcherCode({ arrow: false }), + ), + validCase( + "sync arrow function with filter", + createFollowersDispatcherCode({ async: false }), + ), + validCase( + "sync function expression with filter", + createFollowersDispatcherCode({ async: false, arrow: false }), + ), + ]; +} + +export function createCollectionFilteringInvalidCases( + messageId: string, +): ESLintInvalidTestCase[] { + return [ + invalidCase( + "async arrow function without filter parameter", + createFollowersDispatcherCode({ params: filterless }), + messageId, + ), + invalidCase( + "async function expression without filter", + createFollowersDispatcherCode({ params: filterless, arrow: false }), + messageId, + ), + invalidCase( + "sync arrow function without filter", + createFollowersDispatcherCode({ params: filterless, async: false }), + messageId, + ), + invalidCase( + "sync function expression without filter", + createFollowersDispatcherCode({ + params: filterless, + async: false, + arrow: false, + }), + messageId, + ), + ]; +} diff --git a/packages/lint/src/tests/eslint/actor-assertion-method-required.test.ts b/packages/lint/src/tests/eslint/actor-assertion-method-required.test.ts new file mode 100644 index 000000000..2fc65da0c --- /dev/null +++ b/packages/lint/src/tests/eslint/actor-assertion-method-required.test.ts @@ -0,0 +1,20 @@ +/** + * ESLint tests for actor-assertion-method-required rule. + */ +import { test } from "node:test"; +import { rules, RULE_IDS } from "../../eslint.ts"; +import { runESLintTests } from "../../lib/test-eslint.ts"; +import { + createRequiredInvalidCases, + createRequiredValidCases, +} from "../../lib/test-templates-eslint.ts"; + +const ruleName = RULE_IDS.actorAssertionMethodRequired; +const rule = rules[ruleName]; + +test(`ESLint: ${ruleName}`, () => { + runESLintTests(ruleName, rule, { + valid: createRequiredValidCases("assertionMethod"), + invalid: createRequiredInvalidCases("assertionMethod", "required"), + }); +}); diff --git a/packages/lint/src/tests/eslint/actor-featured-property-mismatch.test.ts b/packages/lint/src/tests/eslint/actor-featured-property-mismatch.test.ts new file mode 100644 index 000000000..d1b80220f --- /dev/null +++ b/packages/lint/src/tests/eslint/actor-featured-property-mismatch.test.ts @@ -0,0 +1,20 @@ +/** + * ESLint tests for actor-featured-property-mismatch rule. + */ +import { test } from "node:test"; +import { rules, RULE_IDS } from "../../eslint.ts"; +import { runESLintTests } from "../../lib/test-eslint.ts"; +import { + createMismatchInvalidCases, + createMismatchValidCases, +} from "../../lib/test-templates-eslint.ts"; + +const ruleName = RULE_IDS.actorFeaturedPropertyMismatch; +const rule = rules[ruleName]; + +test(`ESLint: ${ruleName}`, () => { + runESLintTests(ruleName, rule, { + valid: createMismatchValidCases("featured"), + invalid: createMismatchInvalidCases("featured", "mismatch"), + }); +}); diff --git a/packages/lint/src/tests/eslint/actor-featured-property-required.test.ts b/packages/lint/src/tests/eslint/actor-featured-property-required.test.ts new file mode 100644 index 000000000..cd5df000f --- /dev/null +++ b/packages/lint/src/tests/eslint/actor-featured-property-required.test.ts @@ -0,0 +1,20 @@ +/** + * ESLint tests for actor-featured-property-required rule. + */ +import { test } from "node:test"; +import { rules, RULE_IDS } from "../../eslint.ts"; +import { runESLintTests } from "../../lib/test-eslint.ts"; +import { + createRequiredInvalidCases, + createRequiredValidCases, +} from "../../lib/test-templates-eslint.ts"; + +const ruleName = RULE_IDS.actorFeaturedPropertyRequired; +const rule = rules[ruleName]; + +test(`ESLint: ${ruleName}`, () => { + runESLintTests(ruleName, rule, { + valid: createRequiredValidCases("featured"), + invalid: createRequiredInvalidCases("featured", "required"), + }); +}); diff --git a/packages/lint/src/tests/eslint/actor-featured-tags-property-mismatch.test.ts b/packages/lint/src/tests/eslint/actor-featured-tags-property-mismatch.test.ts new file mode 100644 index 000000000..d1779b224 --- /dev/null +++ b/packages/lint/src/tests/eslint/actor-featured-tags-property-mismatch.test.ts @@ -0,0 +1,20 @@ +/** + * ESLint tests for actor-featured-tags-property-mismatch rule. + */ +import { test } from "node:test"; +import { rules, RULE_IDS } from "../../eslint.ts"; +import { runESLintTests } from "../../lib/test-eslint.ts"; +import { + createMismatchInvalidCases, + createMismatchValidCases, +} from "../../lib/test-templates-eslint.ts"; + +const ruleName = RULE_IDS.actorFeaturedTagsPropertyMismatch; +const rule = rules[ruleName]; + +test(`ESLint: ${ruleName}`, () => { + runESLintTests(ruleName, rule, { + valid: createMismatchValidCases("featuredTags"), + invalid: createMismatchInvalidCases("featuredTags", "mismatch"), + }); +}); diff --git a/packages/lint/src/tests/eslint/actor-featured-tags-property-required.test.ts b/packages/lint/src/tests/eslint/actor-featured-tags-property-required.test.ts new file mode 100644 index 000000000..3dc83b65a --- /dev/null +++ b/packages/lint/src/tests/eslint/actor-featured-tags-property-required.test.ts @@ -0,0 +1,20 @@ +/** + * ESLint tests for actor-featured-tags-property-required rule. + */ +import { test } from "node:test"; +import { rules, RULE_IDS } from "../../eslint.ts"; +import { runESLintTests } from "../../lib/test-eslint.ts"; +import { + createRequiredInvalidCases, + createRequiredValidCases, +} from "../../lib/test-templates-eslint.ts"; + +const ruleName = RULE_IDS.actorFeaturedTagsPropertyRequired; +const rule = rules[ruleName]; + +test(`ESLint: ${ruleName}`, () => { + runESLintTests(ruleName, rule, { + valid: createRequiredValidCases("featuredTags"), + invalid: createRequiredInvalidCases("featuredTags", "required"), + }); +}); diff --git a/packages/lint/src/tests/eslint/actor-followers-property-mismatch.test.ts b/packages/lint/src/tests/eslint/actor-followers-property-mismatch.test.ts new file mode 100644 index 000000000..336496b90 --- /dev/null +++ b/packages/lint/src/tests/eslint/actor-followers-property-mismatch.test.ts @@ -0,0 +1,20 @@ +/** + * ESLint tests for actor-followers-property-mismatch rule. + */ +import { test } from "node:test"; +import { rules, RULE_IDS } from "../../eslint.ts"; +import { runESLintTests } from "../../lib/test-eslint.ts"; +import { + createMismatchInvalidCases, + createMismatchValidCases, +} from "../../lib/test-templates-eslint.ts"; + +const ruleName = RULE_IDS.actorFollowersPropertyMismatch; +const rule = rules[ruleName]; + +test(`ESLint: ${ruleName}`, () => { + runESLintTests(ruleName, rule, { + valid: createMismatchValidCases("followers"), + invalid: createMismatchInvalidCases("followers", "mismatch"), + }); +}); diff --git a/packages/lint/src/tests/eslint/actor-followers-property-required.test.ts b/packages/lint/src/tests/eslint/actor-followers-property-required.test.ts new file mode 100644 index 000000000..f64bdfda6 --- /dev/null +++ b/packages/lint/src/tests/eslint/actor-followers-property-required.test.ts @@ -0,0 +1,20 @@ +/** + * ESLint tests for actor-followers-property-required rule. + */ +import { test } from "node:test"; +import { rules, RULE_IDS } from "../../eslint.ts"; +import { runESLintTests } from "../../lib/test-eslint.ts"; +import { + createRequiredInvalidCases, + createRequiredValidCases, +} from "../../lib/test-templates-eslint.ts"; + +const ruleName = RULE_IDS.actorFollowersPropertyRequired; +const rule = rules[ruleName]; + +test(`ESLint: ${ruleName}`, () => { + runESLintTests(ruleName, rule, { + valid: createRequiredValidCases("followers"), + invalid: createRequiredInvalidCases("followers", "required"), + }); +}); diff --git a/packages/lint/src/tests/eslint/actor-following-property-mismatch.test.ts b/packages/lint/src/tests/eslint/actor-following-property-mismatch.test.ts new file mode 100644 index 000000000..a958d3110 --- /dev/null +++ b/packages/lint/src/tests/eslint/actor-following-property-mismatch.test.ts @@ -0,0 +1,20 @@ +/** + * ESLint tests for actor-following-property-mismatch rule. + */ +import { test } from "node:test"; +import { rules, RULE_IDS } from "../../eslint.ts"; +import { runESLintTests } from "../../lib/test-eslint.ts"; +import { + createMismatchInvalidCases, + createMismatchValidCases, +} from "../../lib/test-templates-eslint.ts"; + +const ruleName = RULE_IDS.actorFollowingPropertyMismatch; +const rule = rules[ruleName]; + +test(`ESLint: ${ruleName}`, () => { + runESLintTests(ruleName, rule, { + valid: createMismatchValidCases("following"), + invalid: createMismatchInvalidCases("following", "mismatch"), + }); +}); diff --git a/packages/lint/src/tests/eslint/actor-following-property-required.test.ts b/packages/lint/src/tests/eslint/actor-following-property-required.test.ts new file mode 100644 index 000000000..4227ddcb3 --- /dev/null +++ b/packages/lint/src/tests/eslint/actor-following-property-required.test.ts @@ -0,0 +1,20 @@ +/** + * ESLint tests for actor-following-property-required rule. + */ +import { test } from "node:test"; +import { rules, RULE_IDS } from "../../eslint.ts"; +import { runESLintTests } from "../../lib/test-eslint.ts"; +import { + createRequiredInvalidCases, + createRequiredValidCases, +} from "../../lib/test-templates-eslint.ts"; + +const ruleName = RULE_IDS.actorFollowingPropertyRequired; +const rule = rules[ruleName]; + +test(`ESLint: ${ruleName}`, () => { + runESLintTests(ruleName, rule, { + valid: createRequiredValidCases("following"), + invalid: createRequiredInvalidCases("following", "required"), + }); +}); diff --git a/packages/lint/src/tests/eslint/actor-id-mismatch.test.ts b/packages/lint/src/tests/eslint/actor-id-mismatch.test.ts new file mode 100644 index 000000000..baa45e913 --- /dev/null +++ b/packages/lint/src/tests/eslint/actor-id-mismatch.test.ts @@ -0,0 +1,20 @@ +/** + * ESLint tests for actor-id-mismatch rule. + */ +import { test } from "node:test"; +import { rules, RULE_IDS } from "../../eslint.ts"; +import { runESLintTests } from "../../lib/test-eslint.ts"; +import { + createMismatchInvalidCases, + createMismatchValidCases, +} from "../../lib/test-templates-eslint.ts"; + +const ruleName = RULE_IDS.actorIdMismatch; +const rule = rules[ruleName]; + +test(`ESLint: ${ruleName}`, () => { + runESLintTests(ruleName, rule, { + valid: createMismatchValidCases("id"), + invalid: createMismatchInvalidCases("id", "mismatch"), + }); +}); diff --git a/packages/lint/src/tests/eslint/actor-id-required.test.ts b/packages/lint/src/tests/eslint/actor-id-required.test.ts new file mode 100644 index 000000000..069b4289e --- /dev/null +++ b/packages/lint/src/tests/eslint/actor-id-required.test.ts @@ -0,0 +1,20 @@ +/** + * ESLint tests for actor-id-required rule. + */ +import { test } from "node:test"; +import { rules, RULE_IDS } from "../../eslint.ts"; +import { runESLintTests } from "../../lib/test-eslint.ts"; +import { + createRequiredInvalidCases, + createRequiredValidCases, +} from "../../lib/test-templates-eslint.ts"; + +const ruleName = RULE_IDS.actorIdRequired; +const rule = rules[ruleName]; + +test(`ESLint: ${ruleName}`, () => { + runESLintTests(ruleName, rule, { + valid: createRequiredValidCases("id"), + invalid: createRequiredInvalidCases("id", "required"), + }); +}); diff --git a/packages/lint/src/tests/eslint/actor-inbox-property-mismatch.test.ts b/packages/lint/src/tests/eslint/actor-inbox-property-mismatch.test.ts new file mode 100644 index 000000000..b391fe2f6 --- /dev/null +++ b/packages/lint/src/tests/eslint/actor-inbox-property-mismatch.test.ts @@ -0,0 +1,20 @@ +/** + * ESLint tests for actor-inbox-property-mismatch rule. + */ +import { test } from "node:test"; +import { rules, RULE_IDS } from "../../eslint.ts"; +import { runESLintTests } from "../../lib/test-eslint.ts"; +import { + createMismatchInvalidCases, + createMismatchValidCases, +} from "../../lib/test-templates-eslint.ts"; + +const ruleName = RULE_IDS.actorInboxPropertyMismatch; +const rule = rules[ruleName]; + +test(`ESLint: ${ruleName}`, () => { + runESLintTests(ruleName, rule, { + valid: createMismatchValidCases("inbox"), + invalid: createMismatchInvalidCases("inbox", "mismatch"), + }); +}); diff --git a/packages/lint/src/tests/eslint/actor-inbox-property-required.test.ts b/packages/lint/src/tests/eslint/actor-inbox-property-required.test.ts new file mode 100644 index 000000000..edfacfd09 --- /dev/null +++ b/packages/lint/src/tests/eslint/actor-inbox-property-required.test.ts @@ -0,0 +1,20 @@ +/** + * ESLint tests for actor-inbox-property-required rule. + */ +import { test } from "node:test"; +import { rules, RULE_IDS } from "../../eslint.ts"; +import { runESLintTests } from "../../lib/test-eslint.ts"; +import { + createRequiredInvalidCases, + createRequiredValidCases, +} from "../../lib/test-templates-eslint.ts"; + +const ruleName = RULE_IDS.actorInboxPropertyRequired; +const rule = rules[ruleName]; + +test(`ESLint: ${ruleName}`, () => { + runESLintTests(ruleName, rule, { + valid: createRequiredValidCases("inbox"), + invalid: createRequiredInvalidCases("inbox", "required"), + }); +}); diff --git a/packages/lint/src/tests/eslint/actor-liked-property-mismatch.test.ts b/packages/lint/src/tests/eslint/actor-liked-property-mismatch.test.ts new file mode 100644 index 000000000..ca86775ca --- /dev/null +++ b/packages/lint/src/tests/eslint/actor-liked-property-mismatch.test.ts @@ -0,0 +1,20 @@ +/** + * ESLint tests for actor-liked-property-mismatch rule. + */ +import { test } from "node:test"; +import { rules, RULE_IDS } from "../../eslint.ts"; +import { runESLintTests } from "../../lib/test-eslint.ts"; +import { + createMismatchInvalidCases, + createMismatchValidCases, +} from "../../lib/test-templates-eslint.ts"; + +const ruleName = RULE_IDS.actorLikedPropertyMismatch; +const rule = rules[ruleName]; + +test(`ESLint: ${ruleName}`, () => { + runESLintTests(ruleName, rule, { + valid: createMismatchValidCases("liked"), + invalid: createMismatchInvalidCases("liked", "mismatch"), + }); +}); diff --git a/packages/lint/src/tests/eslint/actor-liked-property-required.test.ts b/packages/lint/src/tests/eslint/actor-liked-property-required.test.ts new file mode 100644 index 000000000..dad682ee3 --- /dev/null +++ b/packages/lint/src/tests/eslint/actor-liked-property-required.test.ts @@ -0,0 +1,20 @@ +/** + * ESLint tests for actor-liked-property-required rule. + */ +import { test } from "node:test"; +import { rules, RULE_IDS } from "../../eslint.ts"; +import { runESLintTests } from "../../lib/test-eslint.ts"; +import { + createRequiredInvalidCases, + createRequiredValidCases, +} from "../../lib/test-templates-eslint.ts"; + +const ruleName = RULE_IDS.actorLikedPropertyRequired; +const rule = rules[ruleName]; + +test(`ESLint: ${ruleName}`, () => { + runESLintTests(ruleName, rule, { + valid: createRequiredValidCases("liked"), + invalid: createRequiredInvalidCases("liked", "required"), + }); +}); diff --git a/packages/lint/src/tests/eslint/actor-outbox-property-mismatch.test.ts b/packages/lint/src/tests/eslint/actor-outbox-property-mismatch.test.ts new file mode 100644 index 000000000..c6bb73f6e --- /dev/null +++ b/packages/lint/src/tests/eslint/actor-outbox-property-mismatch.test.ts @@ -0,0 +1,20 @@ +/** + * ESLint tests for actor-outbox-property-mismatch rule. + */ +import { test } from "node:test"; +import { rules, RULE_IDS } from "../../eslint.ts"; +import { runESLintTests } from "../../lib/test-eslint.ts"; +import { + createMismatchInvalidCases, + createMismatchValidCases, +} from "../../lib/test-templates-eslint.ts"; + +const ruleName = RULE_IDS.actorOutboxPropertyMismatch; +const rule = rules[ruleName]; + +test(`ESLint: ${ruleName}`, () => { + runESLintTests(ruleName, rule, { + valid: createMismatchValidCases("outbox"), + invalid: createMismatchInvalidCases("outbox", "mismatch"), + }); +}); diff --git a/packages/lint/src/tests/eslint/actor-outbox-property-required.test.ts b/packages/lint/src/tests/eslint/actor-outbox-property-required.test.ts new file mode 100644 index 000000000..a2c062b50 --- /dev/null +++ b/packages/lint/src/tests/eslint/actor-outbox-property-required.test.ts @@ -0,0 +1,20 @@ +/** + * ESLint tests for actor-outbox-property-required rule. + */ +import { test } from "node:test"; +import { rules, RULE_IDS } from "../../eslint.ts"; +import { runESLintTests } from "../../lib/test-eslint.ts"; +import { + createRequiredInvalidCases, + createRequiredValidCases, +} from "../../lib/test-templates-eslint.ts"; + +const ruleName = RULE_IDS.actorOutboxPropertyRequired; +const rule = rules[ruleName]; + +test(`ESLint: ${ruleName}`, () => { + runESLintTests(ruleName, rule, { + valid: createRequiredValidCases("outbox"), + invalid: createRequiredInvalidCases("outbox", "required"), + }); +}); diff --git a/packages/lint/src/tests/eslint/actor-public-key-required.test.ts b/packages/lint/src/tests/eslint/actor-public-key-required.test.ts new file mode 100644 index 000000000..0b68c78e4 --- /dev/null +++ b/packages/lint/src/tests/eslint/actor-public-key-required.test.ts @@ -0,0 +1,20 @@ +/** + * ESLint tests for actor-public-key-required rule. + */ +import { test } from "node:test"; +import { rules, RULE_IDS } from "../../eslint.ts"; +import { runESLintTests } from "../../lib/test-eslint.ts"; +import { + createRequiredInvalidCases, + createRequiredValidCases, +} from "../../lib/test-templates-eslint.ts"; + +const ruleName = RULE_IDS.actorPublicKeyRequired; +const rule = rules[ruleName]; + +test(`ESLint: ${ruleName}`, () => { + runESLintTests(ruleName, rule, { + valid: createRequiredValidCases("publicKey"), + invalid: createRequiredInvalidCases("publicKey", "required"), + }); +}); diff --git a/packages/lint/src/tests/eslint/actor-shared-inbox-property-mismatch.test.ts b/packages/lint/src/tests/eslint/actor-shared-inbox-property-mismatch.test.ts new file mode 100644 index 000000000..6049d859e --- /dev/null +++ b/packages/lint/src/tests/eslint/actor-shared-inbox-property-mismatch.test.ts @@ -0,0 +1,20 @@ +/** + * ESLint tests for actor-shared-inbox-property-mismatch rule. + */ +import { test } from "node:test"; +import { rules, RULE_IDS } from "../../eslint.ts"; +import { runESLintTests } from "../../lib/test-eslint.ts"; +import { + createMismatchInvalidCases, + createMismatchValidCases, +} from "../../lib/test-templates-eslint.ts"; + +const ruleName = RULE_IDS.actorSharedInboxPropertyMismatch; +const rule = rules[ruleName]; + +test(`ESLint: ${ruleName}`, () => { + runESLintTests(ruleName, rule, { + valid: createMismatchValidCases("sharedInbox"), + invalid: createMismatchInvalidCases("sharedInbox", "mismatch"), + }); +}); diff --git a/packages/lint/src/tests/eslint/actor-shared-inbox-property-required.test.ts b/packages/lint/src/tests/eslint/actor-shared-inbox-property-required.test.ts new file mode 100644 index 000000000..427768894 --- /dev/null +++ b/packages/lint/src/tests/eslint/actor-shared-inbox-property-required.test.ts @@ -0,0 +1,20 @@ +/** + * ESLint tests for actor-shared-inbox-property-required rule. + */ +import { test } from "node:test"; +import { rules, RULE_IDS } from "../../eslint.ts"; +import { runESLintTests } from "../../lib/test-eslint.ts"; +import { + createRequiredInvalidCases, + createRequiredValidCases, +} from "../../lib/test-templates-eslint.ts"; + +const ruleName = RULE_IDS.actorSharedInboxPropertyRequired; +const rule = rules[ruleName]; + +test(`ESLint: ${ruleName}`, () => { + runESLintTests(ruleName, rule, { + valid: createRequiredValidCases("sharedInbox"), + invalid: createRequiredInvalidCases("sharedInbox", "required"), + }); +}); diff --git a/packages/lint/src/tests/eslint/collection-filtering-not-implemented.test.ts b/packages/lint/src/tests/eslint/collection-filtering-not-implemented.test.ts new file mode 100644 index 000000000..ec1a62315 --- /dev/null +++ b/packages/lint/src/tests/eslint/collection-filtering-not-implemented.test.ts @@ -0,0 +1,20 @@ +/** + * ESLint tests for collection-filtering-not-implemented rule. + */ +import { test } from "node:test"; +import { rules, RULE_IDS } from "../../eslint.ts"; +import { runESLintTests } from "../../lib/test-eslint.ts"; +import { + createCollectionFilteringInvalidCases, + createCollectionFilteringValidCases, +} from "../../lib/test-templates-eslint.ts"; + +const ruleName = RULE_IDS.collectionFilteringNotImplemented; +const rule = rules[ruleName]; + +test(`ESLint: ${ruleName}`, () => { + runESLintTests(ruleName, rule, { + valid: createCollectionFilteringValidCases(), + invalid: createCollectionFilteringInvalidCases("filterRequired"), + }); +}); From 6f3d2aada2b04b2f5cd6973be161f43061f47fd1 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Sat, 13 Dec 2025 19:56:19 +0000 Subject: [PATCH 24/41] Integrated rule files --- packages/lint/src/index.ts | 182 +++++++-- packages/lint/src/lib/const.ts | 37 +- .../lint/src/lib/mismatch-rule-factory.ts | 357 ++++++------------ packages/lint/src/lib/pred.ts | 115 +++--- packages/lint/src/lib/property-checker.ts | 344 +++++++---------- .../lint/src/lib/required-rule-factory.ts | 185 +++++++-- packages/lint/src/lib/tracker.ts | 10 +- packages/lint/src/lib/types.ts | 93 +++-- packages/lint/src/lib/utils.ts | 49 +++ packages/lint/src/mod.ts | 131 +++---- .../rules/actor-assertion-method-required.ts | 15 +- .../rules/actor-featured-property-mismatch.ts | 15 +- .../rules/actor-featured-property-required.ts | 15 +- .../actor-featured-tags-property-mismatch.ts | 15 +- .../actor-featured-tags-property-required.ts | 15 +- .../actor-followers-property-mismatch.ts | 15 +- .../actor-followers-property-required.ts | 15 +- .../actor-following-property-mismatch.ts | 15 +- .../actor-following-property-required.ts | 15 +- packages/lint/src/rules/actor-id-mismatch.ts | 19 +- packages/lint/src/rules/actor-id-required.ts | 19 +- .../rules/actor-inbox-property-mismatch.ts | 15 +- .../rules/actor-inbox-property-required.ts | 16 +- .../rules/actor-liked-property-mismatch.ts | 15 +- .../rules/actor-liked-property-required.ts | 16 +- .../rules/actor-outbox-property-mismatch.ts | 15 +- .../rules/actor-outbox-property-required.ts | 16 +- .../src/rules/actor-public-key-required.ts | 14 +- .../actor-shared-inbox-property-mismatch.ts | 15 +- .../actor-shared-inbox-property-required.ts | 15 +- .../collection-filtering-not-implemented.ts | 77 ++-- 31 files changed, 1050 insertions(+), 840 deletions(-) diff --git a/packages/lint/src/index.ts b/packages/lint/src/index.ts index e7682d85c..d07863474 100644 --- a/packages/lint/src/index.ts +++ b/packages/lint/src/index.ts @@ -1,34 +1,152 @@ /** - * @fedify/lint - Fedify linting rules and plugins - * - * This package provides lint rules for both Deno.lint and ESLint. - * - * @example ESLint usage: - * ```js - * // eslint.config.js - * import fedifyLint from "@fedify/lint"; - * - * export default [ - * { - * plugins: { - * "@fedify/lint": fedifyLint, - * }, - * rules: { - * "@fedify/lint/actor-id-required": "warn", - * "@fedify/lint/actor-id-mismatch": "error", - * }, - * }, - * ]; - * ``` - * - * Or use the recommended configuration: - * ```js - * // eslint.config.js - * import fedifyLint from "@fedify/lint"; - * - * export default [ - * fedifyLint.configs.recommended, - * ]; - * ``` + * ESLint plugin for Fedify. + * Provides lint rules for validating Fedify federation code. */ -export { default, RULE_IDS, rules } from "./eslint.ts"; +import { keys, pipe, reduceLazy } from "@fxts/core"; +import type { TSESLint } from "@typescript-eslint/utils"; +import metadata from "../deno.json" with { type: "json" }; +import { RULE_IDS } from "./lib/const.ts"; +import { + eslint as actorAssertionMethodRequired, +} from "./rules/actor-assertion-method-required.ts"; +import { + eslint as actorFeaturedPropertyMismatch, +} from "./rules/actor-featured-property-mismatch.ts"; +import { + eslint as actorFeaturedPropertyRequired, +} from "./rules/actor-featured-property-required.ts"; +import { + eslint as actorFeaturedTagsPropertyMismatch, +} from "./rules/actor-featured-tags-property-mismatch.ts"; +import { + eslint as actorFeaturedTagsPropertyRequired, +} from "./rules/actor-featured-tags-property-required.ts"; +import { + eslint as actorFollowersPropertyMismatch, +} from "./rules/actor-followers-property-mismatch.ts"; +import { + eslint as actorFollowersPropertyRequired, +} from "./rules/actor-followers-property-required.ts"; +import { + eslint as actorFollowingPropertyMismatch, +} from "./rules/actor-following-property-mismatch.ts"; +import { + eslint as actorFollowingPropertyRequired, +} from "./rules/actor-following-property-required.ts"; +import { eslint as actorIdMismatch } from "./rules/actor-id-mismatch.ts"; +import { eslint as actorIdRequired } from "./rules/actor-id-required.ts"; +import { + eslint as actorInboxPropertyMismatch, +} from "./rules/actor-inbox-property-mismatch.ts"; +import { + eslint as actorInboxPropertyRequired, +} from "./rules/actor-inbox-property-required.ts"; +import { + eslint as actorLikedPropertyMismatch, +} from "./rules/actor-liked-property-mismatch.ts"; +import { + eslint as actorLikedPropertyRequired, +} from "./rules/actor-liked-property-required.ts"; +import { + eslint as actorOutboxPropertyMismatch, +} from "./rules/actor-outbox-property-mismatch.ts"; +import { + eslint as actorOutboxPropertyRequired, +} from "./rules/actor-outbox-property-required.ts"; +import { + eslint as actorPublicKeyRequired, +} from "./rules/actor-public-key-required.ts"; +import { + eslint as actorSharedInboxPropertyMismatch, +} from "./rules/actor-shared-inbox-property-mismatch.ts"; +import { + eslint as actorSharedInboxPropertyRequired, +} from "./rules/actor-shared-inbox-property-required.ts"; +import { + eslint as collectionFiltering, +} from "./rules/collection-filtering-not-implemented.ts"; + +const rules: Record< + typeof RULE_IDS[keyof typeof RULE_IDS], + TSESLint.RuleModule +> = { + [RULE_IDS.actorIdMismatch]: actorIdMismatch, + [RULE_IDS.actorIdRequired]: actorIdRequired, + [RULE_IDS.actorFollowingPropertyRequired]: actorFollowingPropertyRequired, + [RULE_IDS.actorFollowingPropertyMismatch]: actorFollowingPropertyMismatch, + [RULE_IDS.actorFollowersPropertyRequired]: actorFollowersPropertyRequired, + [RULE_IDS.actorFollowersPropertyMismatch]: actorFollowersPropertyMismatch, + [RULE_IDS.actorOutboxPropertyRequired]: actorOutboxPropertyRequired, + [RULE_IDS.actorOutboxPropertyMismatch]: actorOutboxPropertyMismatch, + [RULE_IDS.actorLikedPropertyRequired]: actorLikedPropertyRequired, + [RULE_IDS.actorLikedPropertyMismatch]: actorLikedPropertyMismatch, + [RULE_IDS.actorFeaturedPropertyRequired]: actorFeaturedPropertyRequired, + [RULE_IDS.actorFeaturedPropertyMismatch]: actorFeaturedPropertyMismatch, + [RULE_IDS.actorFeaturedTagsPropertyRequired]: + actorFeaturedTagsPropertyRequired, + [RULE_IDS.actorFeaturedTagsPropertyMismatch]: + actorFeaturedTagsPropertyMismatch, + [RULE_IDS.actorInboxPropertyRequired]: actorInboxPropertyRequired, + [RULE_IDS.actorInboxPropertyMismatch]: actorInboxPropertyMismatch, + [RULE_IDS.actorSharedInboxPropertyRequired]: actorSharedInboxPropertyRequired, + [RULE_IDS.actorSharedInboxPropertyMismatch]: actorSharedInboxPropertyMismatch, + [RULE_IDS.actorPublicKeyRequired]: actorPublicKeyRequired, + [RULE_IDS.actorAssertionMethodRequired]: actorAssertionMethodRequired, + [RULE_IDS.collectionFilteringNotImplemented]: collectionFiltering, +}; + +/** + * Recommended configuration - enables all rules as warnings + */ +const recommendedRules = pipe( + rules, + keys, + reduceLazy((acc, key) => { + acc[`@fedify/lint/${key}`] = recommendedRuleIds + .includes(key) + ? "error" as const + : "warn" as const; + return acc; + }, {} as Record), +); + +const recommendedRuleIds: (keyof typeof rules)[] = [ + RULE_IDS.actorIdMismatch, + RULE_IDS.actorIdRequired, +]; + +/** + * Strict configuration - enables all rules as errors + */ +const strictRules = pipe( + Object.keys(rules), + (keys) => + keys.reduce((acc, key) => { + acc[`@fedify/lint/${key}`] = "error" as const; + return acc; + }, {} as Record), +); + +// ============================================================================ +// Plugin Export +// ============================================================================ + +const plugin: TSESLint.Linter.Plugin = { + meta: { + name: metadata.name, + version: metadata.version, + }, + rules, + configs: { + recommended: { + plugins: [metadata.name], + rules: recommendedRules, + }, + strict: { + plugins: [metadata.name], + rules: strictRules, + }, + }, +}; + +export default plugin; diff --git a/packages/lint/src/lib/const.ts b/packages/lint/src/lib/const.ts index 7feffc362..793e21520 100644 --- a/packages/lint/src/lib/const.ts +++ b/packages/lint/src/lib/const.ts @@ -1,4 +1,4 @@ -import type { NestedPropertyConfig, PropertyConfig } from "./types.ts"; +import type { PropertyConfig } from "./types.ts"; export const FEDERATION_SETUP = ` import { @@ -14,9 +14,6 @@ const federation = createFederation({ }); ` as const; -// Re-export types for convenience -export type { NestedPropertyConfig, PropertyConfig }; - /** * Mapping of actor property names to their corresponding Context method names * and dispatcher methods. @@ -107,3 +104,35 @@ export const properties = { isKeyProperty: true, }, } as const satisfies Record; + +/** + * Rule IDs for all Fedify lint rules + */ +export const RULE_IDS = { + // Required rules + actorIdRequired: "actor-id-required", + actorFollowingPropertyRequired: "actor-following-property-required", + actorFollowersPropertyRequired: "actor-followers-property-required", + actorOutboxPropertyRequired: "actor-outbox-property-required", + actorLikedPropertyRequired: "actor-liked-property-required", + actorFeaturedPropertyRequired: "actor-featured-property-required", + actorFeaturedTagsPropertyRequired: "actor-featured-tags-property-required", + actorInboxPropertyRequired: "actor-inbox-property-required", + actorSharedInboxPropertyRequired: "actor-shared-inbox-property-required", + actorPublicKeyRequired: "actor-public-key-required", + actorAssertionMethodRequired: "actor-assertion-method-required", + + // Mismatch rules + actorIdMismatch: "actor-id-mismatch", + actorFollowingPropertyMismatch: "actor-following-property-mismatch", + actorFollowersPropertyMismatch: "actor-followers-property-mismatch", + actorOutboxPropertyMismatch: "actor-outbox-property-mismatch", + actorLikedPropertyMismatch: "actor-liked-property-mismatch", + actorFeaturedPropertyMismatch: "actor-featured-property-mismatch", + actorFeaturedTagsPropertyMismatch: "actor-featured-tags-property-mismatch", + actorInboxPropertyMismatch: "actor-inbox-property-mismatch", + actorSharedInboxPropertyMismatch: "actor-shared-inbox-property-mismatch", + + // Collection rules + collectionFilteringNotImplemented: "collection-filtering-not-implemented", +} as const; diff --git a/packages/lint/src/lib/mismatch-rule-factory.ts b/packages/lint/src/lib/mismatch-rule-factory.ts index 3f800fa6e..7e0e4409b 100644 --- a/packages/lint/src/lib/mismatch-rule-factory.ts +++ b/packages/lint/src/lib/mismatch-rule-factory.ts @@ -1,68 +1,54 @@ -import { - filter, - isArray, - isEmpty, - isNil, - isObject, - negate, - pipe, - pipeLazy, - prop, - some, - toArray, -} from "@fxts/core"; +import { isEmpty, negate, some } from "@fxts/core"; +import type { TSESLint } from "@typescript-eslint/utils"; import { actorPropertyMismatch } from "./messages.ts"; import { allOf, - hasIdentifierKey, hasMemberExpressionCallee, isFunction, isNodeName, isNodeType, isSetActorDispatcherCall, } from "./pred.ts"; +import { + createPropertyChecker, + createPropertySearcher, +} from "./property-checker.ts"; import { trackFederationVariables } from "./tracker.ts"; import type { - ASTNode, + AssignmentPattern, + Expression, FunctionNode, + Identifier, MethodCallContext, + Parameter, + PrivateIdentifier, PropertyConfig, + SpreadElement, + TSEmptyBodyFunctionExpression, } from "./types.ts"; -import { eq } from "./utils.ts"; -/** - * Checks if a value is a valid AST node object. - */ -const isASTNode = (node: unknown): node is ASTNode => - isObject(node) && !isNil(node); - -/** - * Filters and casts array items to ASTNode. - */ -const filterASTNodes = (items: unknown[]): ASTNode[] => - pipe( - items, - filter(isObject), - filter((p): p is ASTNode => !isNil(p)), - toArray, - ); - -const isIdentifierWithName = - (name: T) => - (node: N): node is N & { - "type": "Identifier"; - "name": T; - } => allOf(isNodeType("Identifier"), isNodeName(name))(node); +const isIdentifierWithName = (name: T) => +( + node: Expression | SpreadElement | PrivateIdentifier, +): node is Identifier & { "name": T } => + allOf(isNodeType("Identifier"), isNodeName(name))(node); /** * Checks if a node is a CallExpression calling the expected context method. */ const isExpectedMethodCall = ( + { + ctxName, + idName, + methodName, + requiresIdentifier, + }: MethodCallContext, +) => +( node: - | Deno.lint.Expression - | Deno.lint.AssignmentPattern - | Deno.lint.TSEmptyBodyFunctionExpression, - { ctxName, idName, methodName, requiresIdentifier }: MethodCallContext, + | Expression + | AssignmentPattern + | TSEmptyBodyFunctionExpression, ): boolean => { if ( !isNodeType("CallExpression")(node) || @@ -78,202 +64,6 @@ const isExpectedMethodCall = ( )(node.arguments); }; -const isPropertyWithKeyName = (path: string) => -( - node: ASTNode, -): node is { type: "Property"; key: { name: string } & ASTNode } & ASTNode => - allOf( - isNodeType("Property"), - hasIdentifierKey, - pipeLazy( - prop("key")<{ key: { name: string } }>, - prop("name"), - eq(path), - ) as (value: unknown) => boolean, - )(node); - -/** - * Creates a property existence checker for the given property path. - * Only checks if the property exists, not its value. - */ -const createPropertyExistenceChecker = (path: readonly string[]) => { - const checkPropertyExists = - (path: readonly string[]) => (node: ASTNode): boolean => { - if (!isPropertyWithKeyName(path[0])(node)) return false; - - // Base case: last property in path - if (path.length === 1) return true; - - // Nested case: check the nested object or NewExpression - const value = node.value; - - // Handle ObjectExpression: endpoints: { sharedInbox: ... } - if (isNodeType("ObjectExpression")(value)) { - return pipe( - value.properties, - filterASTNodes, - some(checkPropertyExists(path.slice(1))), - ); - } - - // Handle NewExpression: endpoints: new Endpoints({ sharedInbox: ... }) - if (isNodeType("NewExpression")(value)) { - const args = value.arguments; - if (!isArray(args) || args.length === 0) return false; - const firstArg = args[0]; - if (!isASTNode(firstArg) || !isNodeType("ObjectExpression")(firstArg)) { - return false; - } - return pipe( - firstArg.properties, - filterASTNodes, - some(checkPropertyExists(path.slice(1))), - ); - } - - return false; - }; - - return (prop: ASTNode): boolean => - allOf(isASTNode, checkPropertyExists(path))(prop); -}; - -/** - * Creates a property value checker for the given property path. - * Handles both simple properties (e.g., ["id"]) and nested properties (e.g., ["endpoints", "sharedInbox"]). - */ -const createPropertyValueChecker = ( - path: readonly string[], - ctx: MethodCallContext, -) => { - const checkPropertyValue = - (path: readonly string[]) => (prop: ASTNode): boolean => { - if (!isPropertyWithKeyName(path[0])(prop)) return false; - - // Base case: last property in path - const value = prop.value; - if (path.length === 1) { - return isExpectedMethodCall(value, ctx); - } - - // Nested case: check the nested object or NewExpression - // Handle ObjectExpression: endpoints: { sharedInbox: ... } - if (isNodeType("ObjectExpression")(value)) { - return pipe( - value.properties, - filterASTNodes, - some(checkPropertyValue(path.slice(1))), - ); - } - - // Handle NewExpression: endpoints: new Endpoints({ sharedInbox: ... }) - if (isNodeType("NewExpression")(value)) { - const args = value.arguments; - if (!isArray(args) || args.length === 0) return false; - const firstArg = args[0]; - if (!isASTNode(firstArg) || !isNodeType("ObjectExpression")(firstArg)) { - return false; - } - return pipe( - firstArg.properties, - filterASTNodes, - some(checkPropertyValue(path.slice(1))), - ); - } - - return false; - }; - - return (prop: ASTNode): boolean => - allOf(isASTNode, checkPropertyValue(path))(prop); -}; - -/** - * Checks if an ObjectExpression contains a property with the correct method call. - */ -const checkObjectExpression = - (propertyChecker: (prop: ASTNode) => boolean) => - (obj: N): boolean => { - if (!isArray(obj.properties)) return false; - return pipe(obj.properties, filterASTNodes, some(propertyChecker)); - }; - -/** - * Checks if a ConditionalExpression (ternary operator) has the correct property value in both branches. - */ -const checkConditionalExpression = - (propertyChecker: (prop: ASTNode) => boolean) => - (node: Deno.lint.ConditionalExpression): boolean => { - const checkBranch = (branch: ASTNode): boolean => { - // Handle nested ternary operators - if (isNodeType("ConditionalExpression")(branch)) { - return checkConditionalExpression(propertyChecker)(branch); - } - - // Pattern: new SomeClass({ property: ctx.method() }) - if (isNodeType("NewExpression")(branch)) { - return branch.arguments.filter(isNodeType("ObjectExpression")) - .some(checkObjectExpression(propertyChecker)); - } - - return false; - }; - - return checkBranch(node.consequent) && checkBranch(node.alternate); - }; - -/** - * Checks if a ReturnStatement contains the correct property value. - */ -const checkReturnStatement = - (propertyChecker: (prop: ASTNode) => boolean) => (node: ASTNode): boolean => { - const arg = prop("argument")(node); - if (!isASTNode(arg)) return false; - - // Pattern: ternary operator - if (isNodeType("ConditionalExpression")(arg)) { - return checkConditionalExpression(propertyChecker)(arg); - } - - // Pattern: new SomeClass({ property: ctx.method() }) - if (isNodeType("NewExpression")(arg)) { - return arg.arguments.filter(isNodeType("ObjectExpression")) - .some(checkObjectExpression(propertyChecker)); - } - - return false; - }; - -/** - * Recursively checks if a function body contains the correct property value. - */ -const checkFunctionBody = - (propertyChecker: (prop: ASTNode) => boolean) => (node: ASTNode): boolean => { - if (!isASTNode(node)) return false; - - if (isNodeType("ReturnStatement")(node)) { - return checkReturnStatement(propertyChecker)(node); - } - - if (isNodeType("BlockStatement")(node) && Array.isArray(node.body)) { - return node.body.some(checkFunctionBody(propertyChecker)); - } - - // Pattern: arrow function direct return with new expression - // e.g., (ctx, identifier) => new SomeClass({ id: ctx.getActorUri(identifier) }) - if (isNodeType("NewExpression")(node)) { - return node.arguments.filter(isNodeType("ObjectExpression")) - .some(checkObjectExpression(propertyChecker)); - } - - // Pattern: arrow function direct return with ternary - if (isNodeType("ConditionalExpression")(node)) { - return checkConditionalExpression(propertyChecker)(node); - } - - return false; - }; - /** * Extracts parameter names from a function. */ @@ -289,15 +79,17 @@ const extractParams = ( ]; }; -const getNameIfIdentifier = (node: Deno.lint.Parameter): string | null => - isNodeType("Identifier")(node) ? prop("name")(node) : null; +const getNameIfIdentifier = (node: Parameter): string | null => + isNodeType("Identifier")(node as Identifier) + ? (node as Identifier).name + : null; /** * Creates a lint rule that checks if a property uses the correct context method. * * @param config Property configuration containing name, getter, setter, and nested info * @returns A Deno lint rule */ -export const createMismatchRule = ( +export const createMismatchRuleDeno = ( { path, getter, requiresIdentifier = true }: PropertyConfig, ): Deno.lint.Rule => { return { @@ -328,9 +120,8 @@ export const createMismatchRule = ( requiresIdentifier, }; - // Check if the property exists first - const existenceChecker = createPropertyExistenceChecker(path); - const hasProperty = checkFunctionBody(existenceChecker)( + const existenceChecker = createPropertyChecker(Boolean); + const hasProperty = createPropertySearcher(existenceChecker(path))( dispatcherArg.body, ); @@ -338,15 +129,14 @@ export const createMismatchRule = ( if (!hasProperty) return; // Property exists, now check if the value is correct - const valueChecker = createPropertyValueChecker( - path, - methodCallContext, + const propertyChecker = createPropertyChecker( + isExpectedMethodCall(methodCallContext), ); - const hasCorrectValue = checkFunctionBody(valueChecker)( - dispatcherArg.body, + const propertySearcher = createPropertySearcher( + propertyChecker(path), ); - if (!hasCorrectValue) { + if (!propertySearcher(dispatcherArg.body)) { context.report({ node: dispatcherArg, message: actorPropertyMismatch(methodCallContext), @@ -357,3 +147,68 @@ export const createMismatchRule = ( }, }; }; + +export function createMismatchRuleEslint( + { path, getter, requiresIdentifier = true }: PropertyConfig, +): TSESLint.RuleModule { + return { + meta: { + type: "problem", + docs: { + description: `Ensure actor's ${ + path.join(".") + } property uses correct context method`, + }, + schema: [], + messages: { + mismatch: "{{ message }}", + }, + }, + defaultOptions: [], + create(context) { + const tracker = trackFederationVariables(); + + return { + VariableDeclarator: tracker.VariableDeclarator, + + CallExpression(node) { + if ( + !isSetActorDispatcherCall(node) || + !hasMemberExpressionCallee(node) || + !tracker.isFederationObject(node.callee.object) + ) return; + + const dispatcherArg = node.arguments[1]; + if (!isFunction(dispatcherArg)) return; + + const [ctxName, idName] = extractParams(dispatcherArg); + if (!ctxName || !idName) return; + + const methodCallContext: MethodCallContext = { + path: path.join("."), + ctxName, + idName, + methodName: getter, + requiresIdentifier, + }; + + // Property exists, now check if the value is correct + const propertyChecker = createPropertyChecker( + isExpectedMethodCall(methodCallContext), + ); + const propertySearcher = createPropertySearcher( + propertyChecker(path), + ); + + if (!propertySearcher(dispatcherArg.body)) { + context.report({ + node: dispatcherArg, + messageId: "mismatch", + data: { message: actorPropertyMismatch(methodCallContext) }, + }); + } + }, + }; + }, + }; +} diff --git a/packages/lint/src/lib/pred.ts b/packages/lint/src/lib/pred.ts index a1646d4b0..7e2d2d03b 100644 --- a/packages/lint/src/lib/pred.ts +++ b/packages/lint/src/lib/pred.ts @@ -1,16 +1,19 @@ -import { isNil, isObject, isString, negate, pipe, prop } from "@fxts/core"; +import { pipe, prop } from "@fxts/core"; +import type { TSESTree } from "@typescript-eslint/utils"; import type { - ASTNode, + CallExpression, CallMemberExpression, CallMemberExpressionWithIdentified, + Expression, FunctionNode, + Node, + SpreadElement, } from "./types.ts"; import { eq } from "./utils.ts"; interface Predicate { (value: unknown): value is T; } - /** * Combines multiple predicates with AND logic. */ @@ -27,25 +30,20 @@ export function allOf( predicates.every((predicate) => predicate(value)); } -/** - * Type guard to check if a value is a valid AST node. - */ -export const isASTNode = (value: unknown): value is ASTNode => - allOf(isObject, negate(isNil), hasTypeProperty)(value); - -const hasTypeProperty = (node: N): node is N & { "type": string } => - pipe(node as { "type": unknown }, prop("type"), isString) as boolean; +export const anyOf = + (...predicates: ((value: T) => boolean)[]) => (value: T): boolean => + predicates.some((predicate) => predicate(value)); /** * Checks if a node is of a specific type. */ export const isNodeType = - (type: T) => - (node: N): node is { "type": T } & N => + (type: T) => + (node: Node): node is { "type": T } & Node => pipe( node, prop("type"), - eq(type), + eq(type), ) as boolean; /** @@ -59,37 +57,30 @@ export const isNodeName = eq(name), ) as boolean; -/** - * Checks if a node has a key that is an AST node. - */ -export const hasASTNodeKey = (node: N): node is N & { "key": ASTNode } => - pipe(node as { "key": ASTNode }, prop("key"), isASTNode); - /** * Checks if a node's key is an Identifier. */ -export const hasIdentifierKey = ( - node: N, -): node is N & { "key": ASTNode & { type: "Identifier" } } => - pipe( - node as { "key": ASTNode }, - prop("key"), - allOf(isASTNode, isNodeType("Identifier")), - ); +export const hasIdentifierKey = ( + node: T, +): node is T & { "key": Deno.lint.Identifier } => + pipe(node, prop("key"), isNodeType("Identifier")) as boolean; /** * Checks if a node's callee is a MemberExpression. */ export const hasMemberExpressionCallee = ( - node: Deno.lint.CallExpression, + node: CallExpression, ): node is CallMemberExpression => node.callee.type === "MemberExpression"; /** * Checks if a node's callee property is an Identifier. */ export const hasIdentifierProperty = ( - node: CallMemberExpression, + node: CallExpression, ): node is CallMemberExpressionWithIdentified => + "callee" in node && + "property" in node.callee && + "type" in node.callee.property && node.callee.property.type === "Identifier"; /** @@ -97,51 +88,61 @@ export const hasIdentifierProperty = ( */ export const hasMethodName = (methodName: T) => - (node: N): node is N & { + (node: CallExpression): node is CallExpression & { callee: { property: { name: T } }; - } => node.callee.property.name === methodName; + } => + "callee" in node && + "property" in node.callee && + "name" in node.callee.property && + node.callee.property.name === methodName; /** * Checks if a CallExpression has minimum required arguments. */ export const hasMinArguments = - (min: number) => (node: Deno.lint.CallExpression): boolean => - node.arguments.length >= min; - -/** - * Checks if an expression is an arrow function. - */ -export const isArrowFunction = ( - expr: Deno.lint.Expression | Deno.lint.SpreadElement, -): expr is Deno.lint.ArrowFunctionExpression => - isNodeType("ArrowFunctionExpression")(expr); - -/** - * Checks if an expression is a function expression. - */ -export const isFunctionExpression = ( - expr: Deno.lint.Expression | Deno.lint.SpreadElement, -): expr is Deno.lint.FunctionExpression => - isNodeType("FunctionExpression")(expr); + (min: number) => + (node: T): node is Extract => node.arguments.length >= min; /** * Checks if an expression is a function (arrow or regular). */ export const isFunction = ( - expr: Deno.lint.Expression | Deno.lint.SpreadElement, -): expr is FunctionNode => isArrowFunction(expr) || isFunctionExpression(expr); + expr: + | Expression + | SpreadElement, +): expr is FunctionNode => + anyOf( + isNodeType("ArrowFunctionExpression"), + isNodeType("FunctionExpression"), + )(expr); /** * Checks if a CallExpression is a setActorDispatcher call with proper structure. */ -export const isSetActorDispatcherCall = ( - node: N, -): node is N & CallMemberExpressionWithIdentified & { +export const isSetActorDispatcherCall = ( + node: CallExpression, +): node is CallMemberExpressionWithIdentified & { callee: { property: Deno.lint.Identifier & { name: "setActorDispatcher" } }; } => allOf( hasMemberExpressionCallee, - hasIdentifierProperty as (node: unknown) => boolean, - hasMethodName("setActorDispatcher") as (node: unknown) => boolean, + hasIdentifierProperty, + hasMethodName("setActorDispatcher"), hasMinArguments(2), )(node); + +/** + * Checks if an object has a specific property key. + */ +export const hasProp = + (key: K) => + (obj: T): obj is Extract> => + Object.prototype.hasOwnProperty.call(obj, key); + +/** + * Checks if a function has at least n parameters. + */ +export const hasMinParams = (min: number) => (fn: FunctionNode): boolean => + fn.params.length >= min; diff --git a/packages/lint/src/lib/property-checker.ts b/packages/lint/src/lib/property-checker.ts index a2582e91c..da971e14e 100644 --- a/packages/lint/src/lib/property-checker.ts +++ b/packages/lint/src/lib/property-checker.ts @@ -1,261 +1,193 @@ -import { filter, pipe, pipeLazy, prop, some } from "@fxts/core"; import { - allOf, - hasASTNodeKey, - hasIdentifierKey, - isASTNode, - isNodeType, -} from "./pred.ts"; -import type { ASTNode } from "./types.ts"; + isEmpty, + isNil, + isObject, + pipe, + pipeLazy, + prop, + some, + when, +} from "@fxts/core"; +import { allOf, isNodeType } from "./pred.ts"; +import type { + AssignmentPattern, + BlockStatement, + Expression, + NewExpression, + Node, + Property, + PropertyChecker, + ReturnStatement, + SpreadElement, + Statement, + TSEmptyBodyFunctionExpression, + WithIdentifierKey, +} from "./types.ts"; import { eq } from "./utils.ts"; /** - * Checks if a node's key name matches the given property name. + * Checks if a node has a key with a specific name. */ -const keyNameMatches = +const hasKeyName = (propertyName: T) => - (node: N): node is N & { "key": ASTNode & { "name": T } } => + (node: Property): node is Property & WithIdentifierKey => pipe( - node as { "key": { "name": string } }, + node, prop("key"), allOf( - isASTNode, - pipeLazy( - prop("name"), - eq(propertyName), - ) as (value: { name: string }) => boolean, + isNodeType("Identifier"), + pipeLazy(prop("name"), eq(propertyName)) as (node: Node) => boolean, ), ) as boolean; -/** - * Checks if a node has a key with a specific name. - */ -const hasKeyName = - (propertyName: T) => - (node: N): node is N & { "key": ASTNode & { "name": T } } => - allOf( - hasASTNodeKey, - hasIdentifierKey, - keyNameMatches(propertyName), - )(node as { "key": ASTNode & { "name": T } }); - /** * Checks if a node is a Property with an Identifier key of a specific name. */ -const isPropertyWithName = - (propertyName: T) => - (node: N): node is N & { - "type": "Property"; - "key": ASTNode & { "name": T }; - } => - allOf( - isNodeType("Property"), - hasKeyName(propertyName), - )(node); - -/** - * Creates a predicate function that checks if a property has a specific name. - * @param propertyName The name of the property to check for - * @returns A predicate function that checks if the property exists - */ -export const createPropertyChecker = - (propertyName: T) => - (node: unknown): node is ASTNode & { - "type": "Property"; - "key": ASTNode & { "name": T }; - } => allOf(isASTNode, isPropertyWithName(propertyName))(node as ASTNode); - -/** - * Checks if a node has an ObjectExpression value. - */ -const hasObjectExpressionValue = ( - node: Deno.lint.Property, -): node is Deno.lint.Property & { "value": Deno.lint.ObjectExpression } => - pipe( - node as { "value": ASTNode }, - prop("value"), - allOf(isASTNode, isNodeType("ObjectExpression")), - ); - -/** - * Type guard to check if a node is a Property. - */ -const isProperty = (node: ASTNode): node is Deno.lint.Property => - isNodeType("Property")(node); - -/** - * Internal recursive checker for nested property paths. - * This avoids circular dependency between hasNestedProperty and createNestedPropertyChecker. - */ -const checkNestedPropertyPath = - (path: readonly string[]) => (node: unknown): boolean => { - if (!isASTNode(node) || !hasKeyName(path[0])(node)) return false; - - // Base case: single property - if (path.length === 1) { - return isNodeType("Property")(node); - } - - // Recursive case: check nested properties - if (!isProperty(node)) return false; - - const value = node.value; - - // Handle ObjectExpression: endpoints: { sharedInbox: ... } - if (hasObjectExpressionValue(node)) { - const properties = node.value.properties; - return pipe( - properties, - filter(isASTNode), - some(checkNestedPropertyPath(path.slice(1))), - ); - } - - // Handle NewExpression: endpoints: new Endpoints({ sharedInbox: ... }) - if (isNodeType("NewExpression")(value)) { - const args = value.arguments; - if (!Array.isArray(args) || args.length === 0) return false; - const firstArg = args[0]; - if (!isASTNode(firstArg) || !isNodeType("ObjectExpression")(firstArg)) { - return false; - } - return pipe( - firstArg.properties, - filter(isASTNode), - some(checkNestedPropertyPath(path.slice(1))), - ); - } - - return false; - }; +export const isPropertyWithName = (propertyName: T) => +( + node: Property | SpreadElement, +): node is Property & WithIdentifierKey => + allOf( + isNodeType("Property"), + hasKeyName(propertyName), + )(node as Expression & Property); /** * Creates a predicate function that checks if a nested property exists. * @param path Array of property names forming the path (e.g., ["endpoints", "sharedInbox"]) * @returns A predicate function that checks if the nested property exists */ -export const createNestedPropertyChecker = - (path: readonly string[]) => (node: unknown): boolean => - checkNestedPropertyPath(path)(node); +export function createPropertyChecker( + checker: ( + node: + | Expression + | AssignmentPattern + | TSEmptyBodyFunctionExpression, + ) => boolean, +): (path: readonly string[]) => PropertyChecker { + const inner = + ([first, ...rest]: readonly string[]): PropertyChecker => (node) => { + if (!isPropertyWithName(first)(node)) return false; + + // Base case: last property in path + if (isEmpty(rest)) return checker(node.value); + + // Handle NewExpression: endpoints: new Endpoints({ sharedInbox: ... }) + if (isNodeType("NewExpression")(node.value)) { + if (node.value.arguments.length === 0) return false; + const firstArg = node.value.arguments[0]; + if (!isNodeType("ObjectExpression")(firstArg)) return false; + return firstArg.properties.some(inner(rest)); + } + + return false; + }; + return inner; +} /** * Checks if an ObjectExpression node contains a property. * @param propertyChecker The predicate function to check properties * @returns A function that checks the ObjectExpression */ -export const checkObjectExpression = - (propertyChecker: (prop: unknown) => boolean) => (obj: ASTNode): boolean => - pipe( - obj as { properties: unknown[] }, - prop("properties"), - (properties) => - Array.isArray(properties) - ? pipe(properties, filter(isASTNode), some(propertyChecker)) - : false, - ); +const checkObjectExpression = + (propertyChecker: PropertyChecker) => + (obj: Deno.lint.ObjectExpression): boolean => + obj.properties.some(propertyChecker); /** - * Extracts the first argument if it's an ObjectExpression. + * Checks if a ConditionalExpression (ternary operator) has the property in both branches. + * @param propertyChecker The predicate function to check properties + * @returns A function that checks the ConditionalExpression */ -const extractFirstArgument = (node: ASTNode): - | ASTNode & { - type: "ObjectExpression"; - } - | null => - pipe( - node as { arguments: unknown[] }, - prop("arguments"), - (args) => { - if (!Array.isArray(args) || args.length === 0) return null; - const firstArg = args[0]; - return isASTNode(firstArg) && isNodeType("ObjectExpression")(firstArg) - ? firstArg - : null; - }, - ); +const checkConditionalExpression = ( + propertyChecker: PropertyChecker, +) => +(node: Deno.lint.ConditionalExpression): boolean => + [node.consequent, node.alternate].every(checkBranchWith(propertyChecker)); + +// Check if both branches have the property +const checkBranchWith = + (propertyChecker: PropertyChecker) => (branch: Expression): boolean => { + // Handle nested ternary operators + if (isNodeType("ConditionalExpression")(branch)) { + return checkConditionalExpression(propertyChecker)(branch); + } + const objExpr = extractObjectExpression(branch); + return objExpr ? checkObjectExpression(propertyChecker)(objExpr) : false; + }; /** * Extracts ObjectExpression from NewExpression. */ const extractObjectExpression = ( - arg: ASTNode, -): ASTNode & { type: "ObjectExpression" } | null => { - if (isNodeType("NewExpression")(arg)) return extractFirstArgument(arg); + arg: Expression, +): Deno.lint.ObjectExpression | null => { + if (isNodeType("NewExpression")(arg)) { + return extractFirstObjectExpression(arg); + } return null; }; /** - * Checks if a ConditionalExpression (ternary operator) has the property in both branches. - * @param propertyChecker The predicate function to check properties - * @returns A function that checks the ConditionalExpression + * Extracts the first argument if it's an ObjectExpression. */ -const checkConditionalExpression = - (propertyChecker: (prop: unknown) => boolean) => - (node: Deno.lint.ConditionalExpression): boolean => { - const consequent = node.consequent; - const alternate = node.alternate; - - // Check if both branches have the property - const checkBranch = (branch: ASTNode): boolean => { - // Handle nested ternary operators - if (isNodeType("ConditionalExpression")(branch)) { - return checkConditionalExpression(propertyChecker)(branch); - } - const objExpr = extractObjectExpression(branch); - return objExpr ? checkObjectExpression(propertyChecker)(objExpr) : false; - }; - - return checkBranch(consequent) && checkBranch(alternate); - }; +const extractFirstObjectExpression = (node: Deno.lint.NewExpression): + | Deno.lint.ObjectExpression + | null => { + const firstArg = node.arguments[0]; + return isNodeType("ObjectExpression")(firstArg) ? firstArg : null; +}; /** * Checks if a ReturnStatement node contains a property. * @param propertyChecker The predicate function to check properties * @returns A function that checks the ReturnStatement */ -export const checkReturnStatement = - (propertyChecker: (prop: unknown) => boolean) => - ( - node: ASTNode, - ): node is ASTNode & { type: "ReturnStatement"; arg: unknown } => { - const arg = pipe(node as { argument: unknown }, prop("argument")); - if (!isASTNode(arg)) return false; - - // Handle ConditionalExpression (ternary operator) - if (isNodeType("ConditionalExpression")(arg)) { - return checkConditionalExpression(propertyChecker)(arg); - } +export const checkReturnStatement = ( + propertyChecker: PropertyChecker, +) => +(node: Deno.lint.ReturnStatement) => { + const arg = node.argument; + if (isNil(arg)) return false; + + // Handle ConditionalExpression (ternary operator) + if (isNodeType("ConditionalExpression")(arg)) { + return checkConditionalExpression(propertyChecker)(arg); + } - const objExpr = extractObjectExpression(arg); - return objExpr ? checkObjectExpression(propertyChecker)(objExpr) : false; - }; + const objExpr = extractObjectExpression(arg); + return objExpr ? checkObjectExpression(propertyChecker)(objExpr) : false; +}; /** * Creates a function that recursively checks for a property in an AST node. * @param propertyChecker The predicate function to check properties * @returns A recursive function that checks the AST node */ -export const createPropertySearcher = - (propertyChecker: (prop: unknown) => boolean) => - (node: unknown): node is - | Deno.lint.ReturnStatement - | Deno.lint.BlockStatement - | Deno.lint.NewExpression => { - if (!isASTNode(node)) return false; - - if (isNodeType("ReturnStatement")(node)) { +export const createPropertySearcher = (propertyChecker: PropertyChecker) => +( + node: + | Expression + | BlockStatement + | Statement, +): node is + | ReturnStatement + | BlockStatement + | NewExpression => { + switch (node.type) { + case "ReturnStatement": return checkReturnStatement(propertyChecker)(node); - } - - if (isNodeType("BlockStatement")(node)) { + case "BlockStatement": return node.body.some(createPropertySearcher(propertyChecker)); - } - - // Handle arrow function with direct NewExpression body: () => new SomeClass({...}) - if (isNodeType("NewExpression")(node)) { - const objExpr = extractFirstArgument(node); - return objExpr ? checkObjectExpression(propertyChecker)(objExpr) : false; - } - - return false; - }; + case "NewExpression": + return pipe( + node, + extractFirstObjectExpression, + when(isObject, checkObjectExpression(propertyChecker)), + Boolean, + ); + default: + return false; + } +}; diff --git a/packages/lint/src/lib/required-rule-factory.ts b/packages/lint/src/lib/required-rule-factory.ts index 316786298..5dc91b2bb 100644 --- a/packages/lint/src/lib/required-rule-factory.ts +++ b/packages/lint/src/lib/required-rule-factory.ts @@ -1,3 +1,4 @@ +import type { TSESLint, TSESTree } from "@typescript-eslint/utils"; import { actorPropertyRequired } from "./messages.ts"; import { allOf, @@ -8,35 +9,32 @@ import { isSetActorDispatcherCall, } from "./pred.ts"; import { - createNestedPropertyChecker, createPropertyChecker, createPropertySearcher, } from "./property-checker.ts"; import { trackFederationVariables } from "./tracker.ts"; import type { - CallMemberExpression, + CallExpression, CallMemberExpressionWithIdentified, - FunctionNode, PropertyConfig, } from "./types.ts"; /** * Checks if a CallExpression is a specific dispatcher method call. */ -const isDispatcherMethodCall = (methodName: string) => -( - node: Deno.lint.CallExpression, -): node is CallMemberExpression => - allOf( - hasMemberExpressionCallee, - hasIdentifierProperty, - hasMethodName(methodName), - )(node as CallMemberExpressionWithIdentified); +const isDispatcherMethodCall = + (methodName: string) => + (node: CallExpression): node is CallMemberExpressionWithIdentified => + allOf( + hasMemberExpressionCallee, + hasIdentifierProperty, + hasMethodName(methodName), + )(node); /** * Tracks dispatcher method calls on federation objects. */ -export const createDispatcherTracker = ( +const createDispatcherTracker = ( dispatcherMethod: string, federationTracker: ReturnType, ) => { @@ -44,7 +42,9 @@ export const createDispatcherTracker = ( return { isDispatcherConfigured: () => dispatcherConfigured, - checkDispatcherCall: (node: Deno.lint.CallExpression) => { + checkDispatcherCall: ( + node: CallExpression, + ) => { if ( isDispatcherMethodCall(dispatcherMethod)(node) && federationTracker.isFederationObject(node.callee.object) @@ -58,22 +58,86 @@ export const createDispatcherTracker = ( /** * Stores actor dispatcher info for later validation. */ -interface ActorDispatcherInfo { +interface ActorDispatcherInfoDeno { node: Deno.lint.CallExpression; - dispatcherArg: FunctionNode; + dispatcherArg: + | Deno.lint.ArrowFunctionExpression + | Deno.lint.FunctionExpression; +} + +function createRequiredRule< + Context = Deno.lint.RuleContext | TSESLint.RuleContext, + /* CallExpression = Context extends Deno.lint.RuleContext + ? Deno.lint.CallExpression + : TSESTree.CallExpression, */ + ActorDispatcherInfo = Context extends Deno.lint.RuleContext + ? ActorDispatcherInfoDeno + : ActorDispatcherInfoEslint, +>( + config: PropertyConfig, + describe: ( + config: PropertyConfig, + ) => Context extends Deno.lint.RuleContext ? { + message: string; + } + : { + messageId: string; + data: { message: string }; + }, +) { + return (context: Context) => { + const federationTracker = trackFederationVariables(); + const dispatcherTracker = createDispatcherTracker( + config.setter, + federationTracker, + ); + const actorDispatchers: ActorDispatcherInfo[] = []; + + const propertyChecker = createPropertyChecker(Boolean)( + config.path, + ); + const propertySearcher = createPropertySearcher(propertyChecker); + + return { + VariableDeclarator: federationTracker.VariableDeclarator, + + CallExpression(node: CallExpression) { + dispatcherTracker.checkDispatcherCall(node); + + if (!isSetActorDispatcherCall(node)) return; + if (!federationTracker.isFederationObject(node.callee.object)) return; + + const dispatcherArg = node.arguments[1]; + if (isFunction(dispatcherArg)) { + actorDispatchers.push({ node, dispatcherArg } as ActorDispatcherInfo); + } + }, + + "Program:exit"() { + if (!dispatcherTracker.isDispatcherConfigured()) return; + + for ( + const { dispatcherArg } + of actorDispatchers as ActorDispatcherInfoDeno[] + ) { + if (!propertySearcher(dispatcherArg.body)) { + (context as { report: (arg: unknown) => void }).report({ + node: dispatcherArg, + ...describe(config), + }); + } + } + }, + }; + }; } /** * Creates a required rule with the given property configuration. */ -export function createRequiredRule( +export function createRequiredRuleDeno( config: PropertyConfig, ): Deno.lint.Rule { - const propertyChecker = config.path.length === 1 - ? createPropertyChecker(config.path[0]) - : createNestedPropertyChecker(config.path); - const propertySearcher = createPropertySearcher(propertyChecker); - return { create(context) { const federationTracker = trackFederationVariables(); @@ -81,7 +145,7 @@ export function createRequiredRule( config.setter, federationTracker, ); - const actorDispatchers: ActorDispatcherInfo[] = []; + const actorDispatchers: ActorDispatcherInfoDeno[] = []; return { VariableDeclarator: federationTracker.VariableDeclarator, @@ -101,6 +165,9 @@ export function createRequiredRule( "Program:exit"() { if (!dispatcherTracker.isDispatcherConfigured()) return; + const propertyChecker = createPropertyChecker(Boolean)(config.path); + const propertySearcher = createPropertySearcher(propertyChecker); + for (const { dispatcherArg } of actorDispatchers) { if (!propertySearcher(dispatcherArg.body)) { context.report({ @@ -114,3 +181,75 @@ export function createRequiredRule( }, }; } + +interface ActorDispatcherInfoEslint { + node: TSESTree.CallExpression; + dispatcherArg: + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionExpression; +} + +/** + * Creates a required ESLint rule with the given property configuration. + */ + +export function createRequiredRuleEslint( + config: PropertyConfig, +): TSESLint.RuleModule { + return { + meta: { + type: "suggestion", + docs: { + description: `Ensure actor dispatcher returns ${ + config.path.join(".") + } property`, + }, + schema: [], + messages: { + required: "{{ message }}", + }, + }, + defaultOptions: [], + create(context: TSESLint.RuleContext) { + const federationTracker = trackFederationVariables(); + const dispatcherTracker = createDispatcherTracker( + config.setter, + federationTracker, + ); + const actorDispatchers: ActorDispatcherInfoEslint[] = []; + + return { + VariableDeclarator: federationTracker.VariableDeclarator, + + CallExpression(node): void { + dispatcherTracker.checkDispatcherCall(node); + + if (!isSetActorDispatcherCall(node)) return; + if (!federationTracker.isFederationObject(node.callee.object)) return; + + const dispatcherArg = node.arguments[1]; + if (isFunction(dispatcherArg)) { + actorDispatchers.push({ node, dispatcherArg }); + } + }, + + "Program:exit"(): void { + if (!dispatcherTracker.isDispatcherConfigured()) return; + + const propertyChecker = createPropertyChecker(Boolean)(config.path); + const propertySearcher = createPropertySearcher(propertyChecker); + + for (const { dispatcherArg } of actorDispatchers) { + if (!propertySearcher(dispatcherArg.body)) { + context.report({ + node: dispatcherArg, + messageId: "required", + data: { message: actorPropertyRequired(config) }, + }); + } + } + }, + }; + }, + }; +} diff --git a/packages/lint/src/lib/tracker.ts b/packages/lint/src/lib/tracker.ts index 88082f955..50cc74c7d 100644 --- a/packages/lint/src/lib/tracker.ts +++ b/packages/lint/src/lib/tracker.ts @@ -1,10 +1,14 @@ +import type { TSESTree } from "@typescript-eslint/utils"; + /** * Helper to track variable names that store the result of createFederation() or createFederationBuilder() calls */ export function trackFederationVariables() { const federationVariables = new Set(); - const isFederationObject = (obj: Deno.lint.Expression): boolean => { + const isFederationObject = ( + obj: Deno.lint.Expression | TSESTree.Expression, + ): boolean => { switch (obj.type) { case "Identifier": return federationVariables.has(obj.name); @@ -23,7 +27,9 @@ export function trackFederationVariables() { }; return { - VariableDeclarator(node: Deno.lint.VariableDeclarator) { + VariableDeclarator( + node: Deno.lint.VariableDeclarator | TSESTree.VariableDeclarator, + ): void { const init = node.init; const id = node.id; diff --git a/packages/lint/src/lib/types.ts b/packages/lint/src/lib/types.ts index 0eddbeaa2..4d3dd233a 100644 --- a/packages/lint/src/lib/types.ts +++ b/packages/lint/src/lib/types.ts @@ -1,31 +1,63 @@ -interface CallExpressionWithoutCallee { - type: "CallExpression"; - range: Deno.lint.Range; - optional: boolean; - typeArguments: Deno.lint.TSTypeParameterInstantiation | null; - arguments: Array; - parent: Deno.lint.Node; -} -export interface CallMemberExpression extends CallExpressionWithoutCallee { - callee: Deno.lint.MemberExpression; -} +import type { TSESTree } from "@typescript-eslint/utils"; -export interface CallMemberExpressionWithIdentified - extends CallExpressionWithoutCallee { - callee: Deno.lint.MemberExpression & { - property: Deno.lint.Identifier; - }; -} +export type BlockStatement = Deno.lint.BlockStatement | TSESTree.BlockStatement; +export type CallExpression = Deno.lint.CallExpression | TSESTree.CallExpression; +export type Expression = Deno.lint.Expression | TSESTree.Expression; +export type Identifier = Deno.lint.Identifier | TSESTree.Identifier; +export type MemberExpression = + | Deno.lint.MemberExpression + | TSESTree.MemberExpression; +export type NewExpression = Deno.lint.NewExpression | TSESTree.NewExpression; +export type Node = Deno.lint.Node | TSESTree.Node; +export type ObjectExpression = + | Deno.lint.ObjectExpression + | TSESTree.ObjectExpression; +export type Parameter = Deno.lint.Parameter | TSESTree.Parameter; +export type PrivateIdentifier = + | Deno.lint.PrivateIdentifier + | TSESTree.PrivateIdentifier; +export type Property = Deno.lint.Property | TSESTree.Property; +export type ReturnStatement = + | Deno.lint.ReturnStatement + | TSESTree.ReturnStatement; +export type SpreadElement = Deno.lint.SpreadElement | TSESTree.SpreadElement; +export type Statement = Deno.lint.Statement | TSESTree.Statement; + +export type AssignmentPattern = + | Deno.lint.AssignmentPattern + | TSESTree.AssignmentPattern; +export type TSEmptyBodyFunctionExpression = + | Deno.lint.TSEmptyBodyFunctionExpression + | TSESTree.TSEmptyBodyFunctionExpression; export type FunctionNode = | Deno.lint.ArrowFunctionExpression - | Deno.lint.FunctionExpression; + | Deno.lint.FunctionExpression + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionExpression; + +export type CallMemberExpression = CallExpression & { + callee: MemberExpression; +}; + +export type CallMemberExpressionWithIdentified = CallExpression & { + callee: MemberExpression & { + property: Identifier; + }; +}; + +export type PropertyChecker = ( + prop: + | Property + | SpreadElement, +) => boolean; /** * Configuration for nested property wrappers. - * Used when a property needs to be wrapped in a class instance (e.g., `new Endpoints({...})`). + * Used when a property needs to be wrapped in a class instance + * (e.g., `new Endpoints({...})`). */ -export interface NestedPropertyConfig { +interface NestedPropertyConfig { /** Parent property name (e.g., "endpoints") */ parent: string; /** Wrapper class name (e.g., "Endpoints") */ @@ -38,11 +70,17 @@ export interface NestedPropertyConfig { export interface PropertyConfig { /** Property name (e.g., "id", "sharedInbox") */ name: string; - /** Full property path for lint rules (e.g., ["id"], ["endpoints", "sharedInbox"]) */ + /** + * Full property path for lint rules + * (e.g., ["id"], ["endpoints", "sharedInbox"]) + */ path: readonly string[]; /** Context method name to get the URI (e.g., "getActorUri", "getInboxUri") */ getter: string; - /** Dispatcher/Listener method name (e.g., "setActorDispatcher", "setInboxListeners") */ + /** + * Dispatcher/Listener method name + * (e.g., "setActorDispatcher", "setInboxListeners") + */ setter: string; /** Whether the getter requires an identifier parameter (default: true) */ requiresIdentifier: boolean; @@ -52,10 +90,6 @@ export interface PropertyConfig { isKeyProperty?: boolean; } -export type ASTNode = - & { "type": string } - & (Deno.lint.Node | Deno.lint.Parameter); - /** * Context for method call validation. */ @@ -66,3 +100,10 @@ export interface MethodCallContext { methodName: string; requiresIdentifier: boolean; } + +export interface WithIdentifierKey { + key: { + type: "Identifier" | TSESTree.AST_NODE_TYPES.Identifier; + name: T; + }; +} diff --git a/packages/lint/src/lib/utils.ts b/packages/lint/src/lib/utils.ts index 33a88be93..447404cdb 100644 --- a/packages/lint/src/lib/utils.ts +++ b/packages/lint/src/lib/utils.ts @@ -10,6 +10,19 @@ export function getProp( return (obj: S) => obj[key]; } +/** + * Returns the value of the key in the object if it exists, + * otherwise returns second argument as default value. + */ +export function getPropOr( + key: T, + defaultValue?: V, +): (obj: S) => T extends keyof S ? S[T] : V { + return (obj: S) => + ((obj as Record)[key] ?? defaultValue) as // + (T extends keyof S ? S[T] : V); +} + export const eq = (value: S) => (other: T): boolean => value === other; @@ -18,3 +31,39 @@ export const getArticle = (word: string): string => export const endsWith = (suffix: string) => (str: string): boolean => str.endsWith(suffix); +/* +export function replace(searchValue: string | RegExp, replaceValue: string): ( + str: string, +) => string; +export function replace( + searchValue: string | RegExp, + replacer: (substring: string, ...args: unknown[]) => string, +): ( + str: string, +) => string; +export function replace( + searchValue: string | RegExp, + replaceValue: string | ((substring: string, ...args: unknown[]) => string), +): (str: string) => string { + return (str: string) => + typeof replaceValue === "function" + ? str.replace(searchValue, replaceValue) + : str.replace(searchValue, replaceValue); +} + */ +export const replace: { + (searchValue: string | RegExp, replaceValue: string): (str: string) => string; + ( + searchValue: string | RegExp, + replacer: (substring: string, ...args: unknown[]) => string, + ): (str: string) => string; +} = ( + searchValue: string | RegExp, + replaceValue: string | ((substring: string, ...args: unknown[]) => string), +): (str: string) => string => +(str: string): string => + str.replace( + searchValue, + // @ts-ignore tsc cannot infer the type here + replaceValue, + ); diff --git a/packages/lint/src/mod.ts b/packages/lint/src/mod.ts index 64f5f6c76..b745c2aa8 100644 --- a/packages/lint/src/mod.ts +++ b/packages/lint/src/mod.ts @@ -1,91 +1,92 @@ -import actorAssertionMethodRequired, { - ACTOR_ASSERTION_METHOD_REQUIRED, +import { RULE_IDS } from "./lib/const.ts"; +import { + deno as actorAssertionMethodRequired, } from "./rules/actor-assertion-method-required.ts"; -import actorFeaturedPropertyMismatch, { - ACTOR_FEATURED_PROPERTY_MISMATCH, +import { + deno as actorFeaturedPropertyMismatch, } from "./rules/actor-featured-property-mismatch.ts"; -import actorFeaturedPropertyRequired, { - ACTOR_FEATURED_PROPERTY_REQUIRED, +import { + deno as actorFeaturedPropertyRequired, } from "./rules/actor-featured-property-required.ts"; -import actorFeaturedTagsPropertyMismatch, { - ACTOR_FEATURED_TAGS_PROPERTY_MISMATCH, +import { + deno as actorFeaturedTagsPropertyMismatch, } from "./rules/actor-featured-tags-property-mismatch.ts"; -import actorFeaturedTagsPropertyRequired, { - ACTOR_FEATURED_TAGS_PROPERTY_REQUIRED, +import { + deno as actorFeaturedTagsPropertyRequired, } from "./rules/actor-featured-tags-property-required.ts"; -import actorFollowersPropertyMismatch, { - ACTOR_FOLLOWERS_PROPERTY_MISMATCH, +import { + deno as actorFollowersPropertyMismatch, } from "./rules/actor-followers-property-mismatch.ts"; -import actorFollowersPropertyRequired, { - ACTOR_FOLLOWERS_PROPERTY_REQUIRED, +import { + deno as actorFollowersPropertyRequired, } from "./rules/actor-followers-property-required.ts"; -import actorFollowingPropertyMismatch, { - ACTOR_FOLLOWING_PROPERTY_MISMATCH, +import { + deno as actorFollowingPropertyMismatch, } from "./rules/actor-following-property-mismatch.ts"; -import actorFollowingPropertyRequired, { - ACTOR_FOLLOWING_PROPERTY_REQUIRED, +import { + deno as actorFollowingPropertyRequired, } from "./rules/actor-following-property-required.ts"; -import actorIdMismatch, { - ACTOR_ID_MISMATCH, -} from "./rules/actor-id-mismatch.ts"; -import actorIdRequired, { - ACTOR_ID_REQUIRED, -} from "./rules/actor-id-required.ts"; -import actorInboxPropertyMismatch, { - ACTOR_INBOX_PROPERTY_MISMATCH, +import { deno as actorIdMismatch } from "./rules/actor-id-mismatch.ts"; +import { deno as actorIdRequired } from "./rules/actor-id-required.ts"; +import { + deno as actorInboxPropertyMismatch, } from "./rules/actor-inbox-property-mismatch.ts"; -import actorInboxPropertyRequired, { - ACTOR_INBOX_PROPERTY_REQUIRED, +import { + deno as actorInboxPropertyRequired, } from "./rules/actor-inbox-property-required.ts"; -import actorLikedPropertyMismatch, { - ACTOR_LIKED_PROPERTY_MISMATCH, +import { + deno as actorLikedPropertyMismatch, } from "./rules/actor-liked-property-mismatch.ts"; -import actorLikedPropertyRequired, { - ACTOR_LIKED_PROPERTY_REQUIRED, +import { + deno as actorLikedPropertyRequired, } from "./rules/actor-liked-property-required.ts"; -import actorOutboxPropertyMismatch, { - ACTOR_OUTBOX_PROPERTY_MISMATCH, +import { + deno as actorOutboxPropertyMismatch, } from "./rules/actor-outbox-property-mismatch.ts"; -import actorOutboxPropertyRequired, { - ACTOR_OUTBOX_PROPERTY_REQUIRED, +import { + deno as actorOutboxPropertyRequired, } from "./rules/actor-outbox-property-required.ts"; -import actorPublicKeyRequired, { - ACTOR_PUBLIC_KEY_REQUIRED, +import { + deno as actorPublicKeyRequired, } from "./rules/actor-public-key-required.ts"; -import actorSharedInboxPropertyMismatch, { - ACTOR_SHARED_INBOX_PROPERTY_MISMATCH, +import { + deno as actorSharedInboxPropertyMismatch, } from "./rules/actor-shared-inbox-property-mismatch.ts"; -import actorSharedInboxPropertyRequired, { - ACTOR_SHARED_INBOX_PROPERTY_REQUIRED, +import { + deno as actorSharedInboxPropertyRequired, } from "./rules/actor-shared-inbox-property-required.ts"; -import collectionFilteringNotImplemented, { - COLLECTION_FILTERING_NOT_IMPLEMENTED, +import { + deno as collectionFiltering, } from "./rules/collection-filtering-not-implemented.ts"; const plugin: Deno.lint.Plugin = { name: "fedify-lint", rules: { - [ACTOR_ID_MISMATCH]: actorIdMismatch, - [ACTOR_ID_REQUIRED]: actorIdRequired, - [ACTOR_FOLLOWING_PROPERTY_REQUIRED]: actorFollowingPropertyRequired, - [ACTOR_FOLLOWING_PROPERTY_MISMATCH]: actorFollowingPropertyMismatch, - [ACTOR_FOLLOWERS_PROPERTY_REQUIRED]: actorFollowersPropertyRequired, - [ACTOR_FOLLOWERS_PROPERTY_MISMATCH]: actorFollowersPropertyMismatch, - [ACTOR_OUTBOX_PROPERTY_REQUIRED]: actorOutboxPropertyRequired, - [ACTOR_OUTBOX_PROPERTY_MISMATCH]: actorOutboxPropertyMismatch, - [ACTOR_LIKED_PROPERTY_REQUIRED]: actorLikedPropertyRequired, - [ACTOR_LIKED_PROPERTY_MISMATCH]: actorLikedPropertyMismatch, - [ACTOR_FEATURED_PROPERTY_REQUIRED]: actorFeaturedPropertyRequired, - [ACTOR_FEATURED_PROPERTY_MISMATCH]: actorFeaturedPropertyMismatch, - [ACTOR_FEATURED_TAGS_PROPERTY_REQUIRED]: actorFeaturedTagsPropertyRequired, - [ACTOR_FEATURED_TAGS_PROPERTY_MISMATCH]: actorFeaturedTagsPropertyMismatch, - [ACTOR_INBOX_PROPERTY_REQUIRED]: actorInboxPropertyRequired, - [ACTOR_INBOX_PROPERTY_MISMATCH]: actorInboxPropertyMismatch, - [ACTOR_SHARED_INBOX_PROPERTY_REQUIRED]: actorSharedInboxPropertyRequired, - [ACTOR_SHARED_INBOX_PROPERTY_MISMATCH]: actorSharedInboxPropertyMismatch, - [ACTOR_PUBLIC_KEY_REQUIRED]: actorPublicKeyRequired, - [ACTOR_ASSERTION_METHOD_REQUIRED]: actorAssertionMethodRequired, - [COLLECTION_FILTERING_NOT_IMPLEMENTED]: collectionFilteringNotImplemented, + [RULE_IDS.actorIdMismatch]: actorIdMismatch, + [RULE_IDS.actorIdRequired]: actorIdRequired, + [RULE_IDS.actorFollowingPropertyRequired]: actorFollowingPropertyRequired, + [RULE_IDS.actorFollowingPropertyMismatch]: actorFollowingPropertyMismatch, + [RULE_IDS.actorFollowersPropertyRequired]: actorFollowersPropertyRequired, + [RULE_IDS.actorFollowersPropertyMismatch]: actorFollowersPropertyMismatch, + [RULE_IDS.actorOutboxPropertyRequired]: actorOutboxPropertyRequired, + [RULE_IDS.actorOutboxPropertyMismatch]: actorOutboxPropertyMismatch, + [RULE_IDS.actorLikedPropertyRequired]: actorLikedPropertyRequired, + [RULE_IDS.actorLikedPropertyMismatch]: actorLikedPropertyMismatch, + [RULE_IDS.actorFeaturedPropertyRequired]: actorFeaturedPropertyRequired, + [RULE_IDS.actorFeaturedPropertyMismatch]: actorFeaturedPropertyMismatch, + [RULE_IDS.actorFeaturedTagsPropertyRequired]: + actorFeaturedTagsPropertyRequired, + [RULE_IDS.actorFeaturedTagsPropertyMismatch]: + actorFeaturedTagsPropertyMismatch, + [RULE_IDS.actorInboxPropertyRequired]: actorInboxPropertyRequired, + [RULE_IDS.actorInboxPropertyMismatch]: actorInboxPropertyMismatch, + [RULE_IDS.actorSharedInboxPropertyRequired]: + actorSharedInboxPropertyRequired, + [RULE_IDS.actorSharedInboxPropertyMismatch]: + actorSharedInboxPropertyMismatch, + [RULE_IDS.actorPublicKeyRequired]: actorPublicKeyRequired, + [RULE_IDS.actorAssertionMethodRequired]: actorAssertionMethodRequired, + [RULE_IDS.collectionFilteringNotImplemented]: collectionFiltering, }, }; diff --git a/packages/lint/src/rules/actor-assertion-method-required.ts b/packages/lint/src/rules/actor-assertion-method-required.ts index 716f3efef..3c05225ff 100644 --- a/packages/lint/src/rules/actor-assertion-method-required.ts +++ b/packages/lint/src/rules/actor-assertion-method-required.ts @@ -1,11 +1,12 @@ import { properties } from "../lib/const.ts"; -import { createRequiredRule } from "../lib/required-rule-factory.ts"; +import { + createRequiredRuleDeno, + createRequiredRuleEslint, +} from "../lib/required-rule-factory.ts"; -export const ACTOR_ASSERTION_METHOD_REQUIRED = - "actor-assertion-method-required"; - -const actorAssertionMethodRequired: Deno.lint.Rule = createRequiredRule( +export const deno = createRequiredRuleDeno( + properties.assertionMethod, +); +export const eslint = createRequiredRuleEslint( properties.assertionMethod, ); - -export default actorAssertionMethodRequired; diff --git a/packages/lint/src/rules/actor-featured-property-mismatch.ts b/packages/lint/src/rules/actor-featured-property-mismatch.ts index 425215cf4..fa1f43940 100644 --- a/packages/lint/src/rules/actor-featured-property-mismatch.ts +++ b/packages/lint/src/rules/actor-featured-property-mismatch.ts @@ -1,11 +1,12 @@ import { properties } from "../lib/const.ts"; -import { createMismatchRule } from "../lib/mismatch-rule-factory.ts"; +import { + createMismatchRuleDeno, + createMismatchRuleEslint, +} from "../lib/mismatch-rule-factory.ts"; -export const ACTOR_FEATURED_PROPERTY_MISMATCH = - "actor-featured-property-mismatch" as const; - -const actorFeaturedPropertyMismatch: Deno.lint.Rule = createMismatchRule( +export const deno = createMismatchRuleDeno( + properties.featured, +); +export const eslint = createMismatchRuleEslint( properties.featured, ); - -export default actorFeaturedPropertyMismatch; diff --git a/packages/lint/src/rules/actor-featured-property-required.ts b/packages/lint/src/rules/actor-featured-property-required.ts index 77902d7e2..8e268c78d 100644 --- a/packages/lint/src/rules/actor-featured-property-required.ts +++ b/packages/lint/src/rules/actor-featured-property-required.ts @@ -1,11 +1,8 @@ import { properties } from "../lib/const.ts"; -import { createRequiredRule } from "../lib/required-rule-factory.ts"; +import { + createRequiredRuleDeno, + createRequiredRuleEslint, +} from "../lib/required-rule-factory.ts"; -export const ACTOR_FEATURED_PROPERTY_REQUIRED = - "actor-featured-property-required"; - -const actorFeaturedPropertyRequired = createRequiredRule( - properties.featured, -); - -export default actorFeaturedPropertyRequired; +export const deno = createRequiredRuleDeno(properties.featured); +export const eslint = createRequiredRuleEslint(properties.featured); diff --git a/packages/lint/src/rules/actor-featured-tags-property-mismatch.ts b/packages/lint/src/rules/actor-featured-tags-property-mismatch.ts index e1a53d113..a27f892b2 100644 --- a/packages/lint/src/rules/actor-featured-tags-property-mismatch.ts +++ b/packages/lint/src/rules/actor-featured-tags-property-mismatch.ts @@ -1,11 +1,12 @@ import { properties } from "../lib/const.ts"; -import { createMismatchRule } from "../lib/mismatch-rule-factory.ts"; +import { + createMismatchRuleDeno, + createMismatchRuleEslint, +} from "../lib/mismatch-rule-factory.ts"; -export const ACTOR_FEATURED_TAGS_PROPERTY_MISMATCH = - "actor-featured-tags-property-mismatch" as const; - -const actorFeaturedTagsPropertyMismatch: Deno.lint.Rule = createMismatchRule( +export const deno = createMismatchRuleDeno( + properties.featuredTags, +); +export const eslint = createMismatchRuleEslint( properties.featuredTags, ); - -export default actorFeaturedTagsPropertyMismatch; diff --git a/packages/lint/src/rules/actor-featured-tags-property-required.ts b/packages/lint/src/rules/actor-featured-tags-property-required.ts index c34e07190..13988a42a 100644 --- a/packages/lint/src/rules/actor-featured-tags-property-required.ts +++ b/packages/lint/src/rules/actor-featured-tags-property-required.ts @@ -1,11 +1,12 @@ import { properties } from "../lib/const.ts"; -import { createRequiredRule } from "../lib/required-rule-factory.ts"; +import { + createRequiredRuleDeno, + createRequiredRuleEslint, +} from "../lib/required-rule-factory.ts"; -export const ACTOR_FEATURED_TAGS_PROPERTY_REQUIRED = - "actor-featured-tags-property-required"; - -const actorFeaturedTagsPropertyRequired = createRequiredRule( +export const deno = createRequiredRuleDeno( + properties.featuredTags, +); +export const eslint = createRequiredRuleEslint( properties.featuredTags, ); - -export default actorFeaturedTagsPropertyRequired; diff --git a/packages/lint/src/rules/actor-followers-property-mismatch.ts b/packages/lint/src/rules/actor-followers-property-mismatch.ts index 473b233a3..d2021cc44 100644 --- a/packages/lint/src/rules/actor-followers-property-mismatch.ts +++ b/packages/lint/src/rules/actor-followers-property-mismatch.ts @@ -1,11 +1,12 @@ import { properties } from "../lib/const.ts"; -import { createMismatchRule } from "../lib/mismatch-rule-factory.ts"; +import { + createMismatchRuleDeno, + createMismatchRuleEslint, +} from "../lib/mismatch-rule-factory.ts"; -export const ACTOR_FOLLOWERS_PROPERTY_MISMATCH = - "actor-followers-property-mismatch" as const; - -const actorFollowersPropertyMismatch: Deno.lint.Rule = createMismatchRule( +export const deno = createMismatchRuleDeno( + properties.followers, +); +export const eslint = createMismatchRuleEslint( properties.followers, ); - -export default actorFollowersPropertyMismatch; diff --git a/packages/lint/src/rules/actor-followers-property-required.ts b/packages/lint/src/rules/actor-followers-property-required.ts index 7dace83ae..bea9dcfeb 100644 --- a/packages/lint/src/rules/actor-followers-property-required.ts +++ b/packages/lint/src/rules/actor-followers-property-required.ts @@ -1,11 +1,12 @@ import { properties } from "../lib/const.ts"; -import { createRequiredRule } from "../lib/required-rule-factory.ts"; +import { + createRequiredRuleDeno, + createRequiredRuleEslint, +} from "../lib/required-rule-factory.ts"; -export const ACTOR_FOLLOWERS_PROPERTY_REQUIRED = - "actor-followers-property-required"; - -const actorFollowersPropertyRequired = createRequiredRule( +export const deno = createRequiredRuleDeno( + properties.followers, +); +export const eslint = createRequiredRuleEslint( properties.followers, ); - -export default actorFollowersPropertyRequired; diff --git a/packages/lint/src/rules/actor-following-property-mismatch.ts b/packages/lint/src/rules/actor-following-property-mismatch.ts index ed7497d0b..4f710c91c 100644 --- a/packages/lint/src/rules/actor-following-property-mismatch.ts +++ b/packages/lint/src/rules/actor-following-property-mismatch.ts @@ -1,11 +1,12 @@ import { properties } from "../lib/const.ts"; -import { createMismatchRule } from "../lib/mismatch-rule-factory.ts"; +import { + createMismatchRuleDeno, + createMismatchRuleEslint, +} from "../lib/mismatch-rule-factory.ts"; -export const ACTOR_FOLLOWING_PROPERTY_MISMATCH = - "actor-following-property-mismatch" as const; - -const actorFollowingPropertyMismatch: Deno.lint.Rule = createMismatchRule( +export const deno = createMismatchRuleDeno( + properties.following, +); +export const eslint = createMismatchRuleEslint( properties.following, ); - -export default actorFollowingPropertyMismatch; diff --git a/packages/lint/src/rules/actor-following-property-required.ts b/packages/lint/src/rules/actor-following-property-required.ts index 7863b7f9d..7006d5d46 100644 --- a/packages/lint/src/rules/actor-following-property-required.ts +++ b/packages/lint/src/rules/actor-following-property-required.ts @@ -1,11 +1,12 @@ import { properties } from "../lib/const.ts"; -import { createRequiredRule } from "../lib/required-rule-factory.ts"; +import { + createRequiredRuleDeno, + createRequiredRuleEslint, +} from "../lib/required-rule-factory.ts"; -export const ACTOR_FOLLOWING_PROPERTY_REQUIRED = - "actor-following-property-required"; - -const actorFollowingPropertyRequired = createRequiredRule( +export const deno = createRequiredRuleDeno( + properties.following, +); +export const eslint = createRequiredRuleEslint( properties.following, ); - -export default actorFollowingPropertyRequired; diff --git a/packages/lint/src/rules/actor-id-mismatch.ts b/packages/lint/src/rules/actor-id-mismatch.ts index bb8ddea0c..01d55c783 100644 --- a/packages/lint/src/rules/actor-id-mismatch.ts +++ b/packages/lint/src/rules/actor-id-mismatch.ts @@ -1,15 +1,8 @@ import { properties } from "../lib/const.ts"; -import { createMismatchRule } from "../lib/mismatch-rule-factory.ts"; +import { + createMismatchRuleDeno, + createMismatchRuleEslint, +} from "../lib/mismatch-rule-factory.ts"; -export const ACTOR_ID_MISMATCH = "actor-id-mismatch" as const; - -/** - * Lint rule that checks if the actor's `id` property uses the correct - * `ctx.getActorUri(identifier)` method call. - * - * This is a `*-mismatch` rule that validates the VALUE of the property, - * not just its existence. For checking property existence, use `actor-id-required`. - */ -const actorIdMismatch = createMismatchRule(properties.id); - -export default actorIdMismatch; +export const deno = createMismatchRuleDeno(properties.id); +export const eslint = createMismatchRuleEslint(properties.id); diff --git a/packages/lint/src/rules/actor-id-required.ts b/packages/lint/src/rules/actor-id-required.ts index 8cf21d2e3..c5814a815 100644 --- a/packages/lint/src/rules/actor-id-required.ts +++ b/packages/lint/src/rules/actor-id-required.ts @@ -1,15 +1,8 @@ import { properties } from "../lib/const.ts"; -import { createRequiredRule } from "../lib/required-rule-factory.ts"; +import { + createRequiredRuleDeno, + createRequiredRuleEslint, +} from "../lib/required-rule-factory.ts"; -export const ACTOR_ID_REQUIRED = "actor-id-required" as const; - -/** - * Lint rule that checks if the actor's `id` property EXISTS in the returned object. - * - * This is a `*-required` rule that only validates property existence. - * For checking if the value uses the correct `ctx.getActorUri()` method, - * use `actor-id-mismatch`. - */ -const actorIdRequired = createRequiredRule(properties.id); - -export default actorIdRequired; +export const deno = createRequiredRuleDeno(properties.id); +export const eslint = createRequiredRuleEslint(properties.id); diff --git a/packages/lint/src/rules/actor-inbox-property-mismatch.ts b/packages/lint/src/rules/actor-inbox-property-mismatch.ts index 88960f66c..b71472405 100644 --- a/packages/lint/src/rules/actor-inbox-property-mismatch.ts +++ b/packages/lint/src/rules/actor-inbox-property-mismatch.ts @@ -1,11 +1,12 @@ import { properties } from "../lib/const.ts"; -import { createMismatchRule } from "../lib/mismatch-rule-factory.ts"; +import { + createMismatchRuleDeno, + createMismatchRuleEslint, +} from "../lib/mismatch-rule-factory.ts"; -export const ACTOR_INBOX_PROPERTY_MISMATCH = - "actor-inbox-property-mismatch" as const; - -const actorInboxPropertyMismatch: Deno.lint.Rule = createMismatchRule( +export const deno = createMismatchRuleDeno( + properties.inbox, +); +export const eslint = createMismatchRuleEslint( properties.inbox, ); - -export default actorInboxPropertyMismatch; diff --git a/packages/lint/src/rules/actor-inbox-property-required.ts b/packages/lint/src/rules/actor-inbox-property-required.ts index 5f066d5cf..862cc7dac 100644 --- a/packages/lint/src/rules/actor-inbox-property-required.ts +++ b/packages/lint/src/rules/actor-inbox-property-required.ts @@ -1,8 +1,12 @@ import { properties } from "../lib/const.ts"; -import { createRequiredRule } from "../lib/required-rule-factory.ts"; +import { + createRequiredRuleDeno, + createRequiredRuleEslint, +} from "../lib/required-rule-factory.ts"; -export const ACTOR_INBOX_PROPERTY_REQUIRED = "actor-inbox-property-required"; - -const actorInboxPropertyRequired = createRequiredRule(properties.inbox); - -export default actorInboxPropertyRequired; +export const deno = createRequiredRuleDeno( + properties.inbox, +); +export const eslint = createRequiredRuleEslint( + properties.inbox, +); diff --git a/packages/lint/src/rules/actor-liked-property-mismatch.ts b/packages/lint/src/rules/actor-liked-property-mismatch.ts index 5777331df..ad3833eda 100644 --- a/packages/lint/src/rules/actor-liked-property-mismatch.ts +++ b/packages/lint/src/rules/actor-liked-property-mismatch.ts @@ -1,11 +1,12 @@ import { properties } from "../lib/const.ts"; -import { createMismatchRule } from "../lib/mismatch-rule-factory.ts"; +import { + createMismatchRuleDeno, + createMismatchRuleEslint, +} from "../lib/mismatch-rule-factory.ts"; -export const ACTOR_LIKED_PROPERTY_MISMATCH = - "actor-liked-property-mismatch" as const; - -const actorLikedPropertyMismatch: Deno.lint.Rule = createMismatchRule( +export const deno = createMismatchRuleDeno( + properties.liked, +); +export const eslint = createMismatchRuleEslint( properties.liked, ); - -export default actorLikedPropertyMismatch; diff --git a/packages/lint/src/rules/actor-liked-property-required.ts b/packages/lint/src/rules/actor-liked-property-required.ts index 9a2890bf2..000135c40 100644 --- a/packages/lint/src/rules/actor-liked-property-required.ts +++ b/packages/lint/src/rules/actor-liked-property-required.ts @@ -1,8 +1,12 @@ import { properties } from "../lib/const.ts"; -import { createRequiredRule } from "../lib/required-rule-factory.ts"; +import { + createRequiredRuleDeno, + createRequiredRuleEslint, +} from "../lib/required-rule-factory.ts"; -export const ACTOR_LIKED_PROPERTY_REQUIRED = "actor-liked-property-required"; - -const actorLikedPropertyRequired = createRequiredRule(properties.liked); - -export default actorLikedPropertyRequired; +export const deno = createRequiredRuleDeno( + properties.liked, +); +export const eslint = createRequiredRuleEslint( + properties.liked, +); diff --git a/packages/lint/src/rules/actor-outbox-property-mismatch.ts b/packages/lint/src/rules/actor-outbox-property-mismatch.ts index 4589d5443..515b5a08a 100644 --- a/packages/lint/src/rules/actor-outbox-property-mismatch.ts +++ b/packages/lint/src/rules/actor-outbox-property-mismatch.ts @@ -1,11 +1,12 @@ import { properties } from "../lib/const.ts"; -import { createMismatchRule } from "../lib/mismatch-rule-factory.ts"; +import { + createMismatchRuleDeno, + createMismatchRuleEslint, +} from "../lib/mismatch-rule-factory.ts"; -export const ACTOR_OUTBOX_PROPERTY_MISMATCH = - "actor-outbox-property-mismatch" as const; - -const actorOutboxPropertyMismatch: Deno.lint.Rule = createMismatchRule( +export const deno = createMismatchRuleDeno( + properties.outbox, +); +export const eslint = createMismatchRuleEslint( properties.outbox, ); - -export default actorOutboxPropertyMismatch; diff --git a/packages/lint/src/rules/actor-outbox-property-required.ts b/packages/lint/src/rules/actor-outbox-property-required.ts index c4f5c3082..bf75a95fd 100644 --- a/packages/lint/src/rules/actor-outbox-property-required.ts +++ b/packages/lint/src/rules/actor-outbox-property-required.ts @@ -1,8 +1,12 @@ import { properties } from "../lib/const.ts"; -import { createRequiredRule } from "../lib/required-rule-factory.ts"; +import { + createRequiredRuleDeno, + createRequiredRuleEslint, +} from "../lib/required-rule-factory.ts"; -export const ACTOR_OUTBOX_PROPERTY_REQUIRED = "actor-outbox-property-required"; - -const actorOutboxPropertyRequired = createRequiredRule(properties.outbox); - -export default actorOutboxPropertyRequired; +export const deno = createRequiredRuleDeno( + properties.outbox, +); +export const eslint = createRequiredRuleEslint( + properties.outbox, +); diff --git a/packages/lint/src/rules/actor-public-key-required.ts b/packages/lint/src/rules/actor-public-key-required.ts index 65d826f5e..0068b226b 100644 --- a/packages/lint/src/rules/actor-public-key-required.ts +++ b/packages/lint/src/rules/actor-public-key-required.ts @@ -1,10 +1,12 @@ import { properties } from "../lib/const.ts"; -import { createRequiredRule } from "../lib/required-rule-factory.ts"; +import { + createRequiredRuleDeno, + createRequiredRuleEslint, +} from "../lib/required-rule-factory.ts"; -export const ACTOR_PUBLIC_KEY_REQUIRED = "actor-public-key-required"; - -const actorPublicKeyRequired: Deno.lint.Rule = createRequiredRule( +export const deno = createRequiredRuleDeno( + properties.publicKey, +); +export const eslint = createRequiredRuleEslint( properties.publicKey, ); - -export default actorPublicKeyRequired; diff --git a/packages/lint/src/rules/actor-shared-inbox-property-mismatch.ts b/packages/lint/src/rules/actor-shared-inbox-property-mismatch.ts index 4093652eb..bfbe8fc1c 100644 --- a/packages/lint/src/rules/actor-shared-inbox-property-mismatch.ts +++ b/packages/lint/src/rules/actor-shared-inbox-property-mismatch.ts @@ -1,11 +1,12 @@ import { properties } from "../lib/const.ts"; -import { createMismatchRule } from "../lib/mismatch-rule-factory.ts"; +import { + createMismatchRuleDeno, + createMismatchRuleEslint, +} from "../lib/mismatch-rule-factory.ts"; -export const ACTOR_SHARED_INBOX_PROPERTY_MISMATCH = - "actor-shared-inbox-property-mismatch" as const; - -const actorSharedInboxPropertyMismatch: Deno.lint.Rule = createMismatchRule( +export const deno = createMismatchRuleDeno( + properties.sharedInbox, +); +export const eslint = createMismatchRuleEslint( properties.sharedInbox, ); - -export default actorSharedInboxPropertyMismatch; diff --git a/packages/lint/src/rules/actor-shared-inbox-property-required.ts b/packages/lint/src/rules/actor-shared-inbox-property-required.ts index ccd1295c6..ee70bb1be 100644 --- a/packages/lint/src/rules/actor-shared-inbox-property-required.ts +++ b/packages/lint/src/rules/actor-shared-inbox-property-required.ts @@ -1,11 +1,12 @@ import { properties } from "../lib/const.ts"; -import { createRequiredRule } from "../lib/required-rule-factory.ts"; +import { + createRequiredRuleDeno, + createRequiredRuleEslint, +} from "../lib/required-rule-factory.ts"; -export const ACTOR_SHARED_INBOX_PROPERTY_REQUIRED = - "actor-shared-inbox-property-required"; - -const actorSharedInboxPropertyRequired = createRequiredRule( +export const deno = createRequiredRuleDeno( + properties.sharedInbox, +); +export const eslint = createRequiredRuleEslint( properties.sharedInbox, ); - -export default actorSharedInboxPropertyRequired; diff --git a/packages/lint/src/rules/collection-filtering-not-implemented.ts b/packages/lint/src/rules/collection-filtering-not-implemented.ts index 31dd8ba28..8b8b1d77e 100644 --- a/packages/lint/src/rules/collection-filtering-not-implemented.ts +++ b/packages/lint/src/rules/collection-filtering-not-implemented.ts @@ -1,18 +1,13 @@ +import type { TSESLint } from "@typescript-eslint/utils"; import { + COLLECTION_FILTERING_NOT_IMPLEMENTED_ERROR, COLLECTION_FILTERING_NOT_IMPLEMENTED_ERROR as message, } from "../lib/messages.ts"; -import { - allOf, - hasIdentifierProperty, - hasMemberExpressionCallee, - hasMethodName, - hasMinArguments, - isFunction, -} from "../lib/pred.ts"; +import { hasMinParams, isFunction } from "../lib/pred.ts"; import { trackFederationVariables } from "../lib/tracker.ts"; import type { + CallExpression, CallMemberExpressionWithIdentified, - FunctionNode, } from "../lib/types.ts"; export const COLLECTION_FILTERING_NOT_IMPLEMENTED = @@ -24,29 +19,25 @@ export const COLLECTION_FILTERING_NOT_IMPLEMENTED = * followers collection synchronization. * See: https://fedify.dev/manual/collections#filtering-by-server */ -const FOLLOWERS_DISPATCHER_METHOD = "setFollowersDispatcher" as const; +const FILTER_NEEDED = ["setFollowersDispatcher"]; /** * Checks if a node is a setFollowersDispatcher call. */ const isFollowersDispatcherCall = ( - node: Deno.lint.CallExpression, -): node is Deno.lint.CallExpression & CallMemberExpressionWithIdentified => - allOf( - hasMemberExpressionCallee, - hasIdentifierProperty, - hasMinArguments(2), - hasMethodName(FOLLOWERS_DISPATCHER_METHOD), - )(node as Deno.lint.CallExpression & CallMemberExpressionWithIdentified); + node: CallExpression, +): node is CallMemberExpressionWithIdentified => + "callee" in node && + node.callee && + node.callee.type === "MemberExpression" && + node.callee.property.type === "Identifier" && + FILTER_NEEDED.includes(node.callee.property.name); /** * Checks if a function node has the filter parameter (4th parameter). * CollectionDispatcher signature: (context, identifier, cursor, filter?) => ... */ -const hasFilterParameter = (fn: FunctionNode): boolean => { - // Filter is the 4th parameter (index 3) - return fn.params.length >= 4; -}; +const hasFilterParameter = hasMinParams(4); /** * Lint rule: collection-filtering-not-implemented @@ -81,7 +72,7 @@ const hasFilterParameter = (fn: FunctionNode): boolean => { * ); * ``` */ -const collectionFilteringNotImplementedRule: Deno.lint.Rule = { +export const deno: Deno.lint.Rule = { create(context) { const federationTracker = trackFederationVariables(); @@ -109,4 +100,42 @@ const collectionFilteringNotImplementedRule: Deno.lint.Rule = { }, }; -export default collectionFilteringNotImplementedRule; +export const eslint: TSESLint.RuleModule< + string, + unknown[] +> = { + meta: { + type: "suggestion", + docs: { + description: "Ensure followers dispatcher implements filtering", + }, + schema: [], + messages: { + filterRequired: COLLECTION_FILTERING_NOT_IMPLEMENTED_ERROR, + }, + }, + defaultOptions: [], + create(context) { + const federationTracker = trackFederationVariables(); + + return { + VariableDeclarator: federationTracker.VariableDeclarator, + CallExpression(node): void { + if (!isFollowersDispatcherCall(node)) return; + if (!federationTracker.isFederationObject(node.callee.object)) return; + + // Get the dispatcher callback (2nd argument) + const dispatcherArg = node.arguments[1]; + if (!isFunction(dispatcherArg)) return; + + // Check if the callback has the filter parameter (4th parameter) + if (!hasFilterParameter(dispatcherArg)) { + context.report({ + node: dispatcherArg, + messageId: "filterRequired", + }); + } + }, + }; + }, +}; From d871d33f4584e6814389a599ab18deeb5e59f8b5 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Sat, 13 Dec 2025 19:58:00 +0000 Subject: [PATCH 25/41] Integrated test files --- packages/lint/src/lib/test-templates.ts | 1381 +++++++---------- packages/lint/src/lib/test.ts | 116 +- .../actor-assertion-method-required.test.ts | 101 +- .../actor-featured-property-mismatch.test.ts | 8 +- .../actor-featured-property-required.test.ts | 8 +- ...or-featured-tags-property-mismatch.test.ts | 8 +- ...or-featured-tags-property-required.test.ts | 8 +- .../actor-followers-property-mismatch.test.ts | 8 +- .../actor-followers-property-required.test.ts | 8 +- .../actor-following-property-mismatch.test.ts | 8 +- .../actor-following-property-required.test.ts | 8 +- .../lint/src/tests/actor-id-mismatch.test.ts | 119 +- .../lint/src/tests/actor-id-required.test.ts | 259 ++-- .../actor-inbox-property-mismatch.test.ts | 8 +- .../actor-inbox-property-required.test.ts | 8 +- .../actor-liked-property-mismatch.test.ts | 8 +- .../actor-liked-property-required.test.ts | 8 +- .../actor-outbox-property-mismatch.test.ts | 8 +- .../actor-outbox-property-required.test.ts | 8 +- .../tests/actor-public-key-required.test.ts | 101 +- ...tor-shared-inbox-property-mismatch.test.ts | 8 +- ...tor-shared-inbox-property-required.test.ts | 8 +- ...llection-filtering-not-implemented.test.ts | 175 +-- .../actor-assertion-method-required.test.ts | 20 - .../actor-featured-property-mismatch.test.ts | 20 - .../actor-featured-property-required.test.ts | 20 - ...or-featured-tags-property-mismatch.test.ts | 20 - ...or-featured-tags-property-required.test.ts | 20 - .../actor-followers-property-mismatch.test.ts | 20 - .../actor-followers-property-required.test.ts | 20 - .../actor-following-property-mismatch.test.ts | 20 - .../actor-following-property-required.test.ts | 20 - .../tests/eslint/actor-id-mismatch.test.ts | 20 - .../tests/eslint/actor-id-required.test.ts | 20 - .../actor-inbox-property-mismatch.test.ts | 20 - .../actor-inbox-property-required.test.ts | 20 - .../actor-liked-property-mismatch.test.ts | 20 - .../actor-liked-property-required.test.ts | 20 - .../actor-outbox-property-mismatch.test.ts | 20 - .../actor-outbox-property-required.test.ts | 20 - .../eslint/actor-public-key-required.test.ts | 20 - ...tor-shared-inbox-property-mismatch.test.ts | 20 - ...tor-shared-inbox-property-required.test.ts | 20 - ...llection-filtering-not-implemented.test.ts | 20 - packages/lint/src/tests/integration.test.ts | 22 +- 45 files changed, 1162 insertions(+), 1660 deletions(-) delete mode 100644 packages/lint/src/tests/eslint/actor-assertion-method-required.test.ts delete mode 100644 packages/lint/src/tests/eslint/actor-featured-property-mismatch.test.ts delete mode 100644 packages/lint/src/tests/eslint/actor-featured-property-required.test.ts delete mode 100644 packages/lint/src/tests/eslint/actor-featured-tags-property-mismatch.test.ts delete mode 100644 packages/lint/src/tests/eslint/actor-featured-tags-property-required.test.ts delete mode 100644 packages/lint/src/tests/eslint/actor-followers-property-mismatch.test.ts delete mode 100644 packages/lint/src/tests/eslint/actor-followers-property-required.test.ts delete mode 100644 packages/lint/src/tests/eslint/actor-following-property-mismatch.test.ts delete mode 100644 packages/lint/src/tests/eslint/actor-following-property-required.test.ts delete mode 100644 packages/lint/src/tests/eslint/actor-id-mismatch.test.ts delete mode 100644 packages/lint/src/tests/eslint/actor-id-required.test.ts delete mode 100644 packages/lint/src/tests/eslint/actor-inbox-property-mismatch.test.ts delete mode 100644 packages/lint/src/tests/eslint/actor-inbox-property-required.test.ts delete mode 100644 packages/lint/src/tests/eslint/actor-liked-property-mismatch.test.ts delete mode 100644 packages/lint/src/tests/eslint/actor-liked-property-required.test.ts delete mode 100644 packages/lint/src/tests/eslint/actor-outbox-property-mismatch.test.ts delete mode 100644 packages/lint/src/tests/eslint/actor-outbox-property-required.test.ts delete mode 100644 packages/lint/src/tests/eslint/actor-public-key-required.test.ts delete mode 100644 packages/lint/src/tests/eslint/actor-shared-inbox-property-mismatch.test.ts delete mode 100644 packages/lint/src/tests/eslint/actor-shared-inbox-property-required.test.ts delete mode 100644 packages/lint/src/tests/eslint/collection-filtering-not-implemented.test.ts diff --git a/packages/lint/src/lib/test-templates.ts b/packages/lint/src/lib/test-templates.ts index 1bc1059e9..b7a8f1c61 100644 --- a/packages/lint/src/lib/test-templates.ts +++ b/packages/lint/src/lib/test-templates.ts @@ -1,23 +1,19 @@ -import { properties, type PropertyConfig } from "./const.ts"; +import type { TSESLint } from "@typescript-eslint/utils"; +import { properties } from "./const.ts"; import { actorPropertyMismatch, actorPropertyRequired } from "./messages.ts"; -import { testDenoLint } from "./test.ts"; -import type { MethodCallContext } from "./types.ts"; - -// ============================================================================= -// Types -// ============================================================================= +import lintTest from "./test.ts"; +import type { MethodCallContext, PropertyConfig } from "./types.ts"; type PropertyKey = keyof typeof properties; interface TestConfig { - rule: Deno.lint.Rule; + rule: { + deno: Deno.lint.Rule; + eslint: TSESLint.RuleModule; + }; ruleName: string; } -// ============================================================================= -// Common Code Snippets -// ============================================================================= - const createDispatcherCode = (content: string) => ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { ${content} @@ -46,10 +42,6 @@ const createSeparateDispatcherCode = ( return isBefore ? `${dispatcher}\n${actor}` : `${actor}\n${dispatcher}`; }; -// ============================================================================= -// Property Code Generation Utilities -// ============================================================================= - /** * Generates the method call code for a property. * @param getter The getter method name @@ -153,133 +145,113 @@ export function createRequiredDispatcherRuleTests( return { // ✅ Good - non-Federation object - "non-federation object": () => { - testDenoLint({ - code: createDispatcherCode(createTestCode(propertyKey, false)), - rule, - ruleName, - federationSetup: ` + "non-federation object": lintTest({ + code: createDispatcherCode(createTestCode(propertyKey, false)), + rule, + ruleName, + federationSetup: ` const federation = { setActorDispatcher: () => {} }; `, - }); - }, + }), // ✅ Good - dispatcher NOT configured - "dispatcher not configured": () => { - testDenoLint({ - code: createDispatcherCode(createTestCode(propertyKey, false)), - rule, - ruleName, - }); - }, + "dispatcher not configured": lintTest({ + code: createDispatcherCode(createTestCode(propertyKey, false)), + rule, + ruleName, + }), // ✅ Good - dispatcher configured BEFORE (chained) - "dispatcher before chained with property": () => { - testDenoLint({ - code: createChainedDispatcherCode( - createTestCode(propertyKey, true), - prop.setter, - ), - rule, - ruleName, - }); - }, + "dispatcher before chained with property": lintTest({ + code: createChainedDispatcherCode( + createTestCode(propertyKey, true), + prop.setter, + ), + rule, + ruleName, + }), // ✅ Good - dispatcher configured BEFORE (separate) - "dispatcher before separate with property": () => { - testDenoLint({ - code: createSeparateDispatcherCode( - createTestCode(propertyKey, true), - prop.setter, - true, - ), - rule, - ruleName, - }); - }, + "dispatcher before separate with property": lintTest({ + code: createSeparateDispatcherCode( + createTestCode(propertyKey, true), + prop.setter, + true, + ), + rule, + ruleName, + }), // ✅ Good - dispatcher configured AFTER (chained) - "dispatcher after chained with property": () => { - testDenoLint({ - code: createChainedDispatcherCode( - createTestCode(propertyKey, true), - prop.setter, - ), - rule, - ruleName, - }); - }, + "dispatcher after chained with property": lintTest({ + code: createChainedDispatcherCode( + createTestCode(propertyKey, true), + prop.setter, + ), + rule, + ruleName, + }), // ✅ Good - dispatcher configured AFTER (separate) - "dispatcher after separate with property": () => { - testDenoLint({ - code: createSeparateDispatcherCode( - createTestCode(propertyKey, true), - prop.setter, - false, - ), - rule, - ruleName, - }); - }, + "dispatcher after separate with property": lintTest({ + code: createSeparateDispatcherCode( + createTestCode(propertyKey, true), + prop.setter, + false, + ), + rule, + ruleName, + }), // ❌ Bad - dispatcher configured, property missing - "dispatcher configured property missing": () => { - testDenoLint({ - code: createChainedDispatcherCode( - createTestCode(propertyKey, false), - prop.setter, - ), - rule, - ruleName, - expectedError, - }); - }, + "dispatcher configured property missing": lintTest({ + code: createChainedDispatcherCode( + createTestCode(propertyKey, false), + prop.setter, + ), + rule, + ruleName, + expectedError, + }), // ❌ Bad - dispatcher before (separate), property missing - "dispatcher before separate property missing": () => { - testDenoLint({ - code: createSeparateDispatcherCode( - createTestCode(propertyKey, false), - prop.setter, - true, - ), - rule, - ruleName, - expectedError, - }); - }, + "dispatcher before separate property missing": lintTest({ + code: createSeparateDispatcherCode( + createTestCode(propertyKey, false), + prop.setter, + true, + ), + rule, + ruleName, + expectedError, + }), // ❌ Bad - dispatcher after (separate), property missing - "dispatcher after separate property missing": () => { - testDenoLint({ - code: createSeparateDispatcherCode( - createTestCode(propertyKey, false), - prop.setter, - false, - ), - rule, - ruleName, - expectedError, - }); - }, + "dispatcher after separate property missing": lintTest({ + code: createSeparateDispatcherCode( + createTestCode(propertyKey, false), + prop.setter, + false, + ), + rule, + ruleName, + expectedError, + }), // ❌ Bad - variable assignment without property - "variable assignment without property": () => { - testDenoLint({ - code: createChainedDispatcherCode( - `const actor = new Person({ + "variable assignment without property": lintTest({ + code: createChainedDispatcherCode( + `const actor = new Person({ id: ctx.getActorUri(identifier), name: "John Doe", }); return actor;`, - prop.setter, - ), - rule, - ruleName, - expectedError, - }); - }, + prop.setter, + ), + rule, + ruleName, + expectedError, + }), }; } @@ -310,133 +282,113 @@ export function createRequiredListenerRuleTests( return { // ✅ Good - non-Federation object - "non-federation object": () => { - testDenoLint({ - code: createDispatcherCode(createActorCode(false)), - rule, - ruleName, - federationSetup: ` + "non-federation object": lintTest({ + code: createDispatcherCode(createActorCode(false)), + rule, + ruleName, + federationSetup: ` const federation = { setActorDispatcher: () => {} }; `, - }); - }, + }), // ✅ Good - listeners NOT configured - "listeners not configured": () => { - testDenoLint({ - code: createDispatcherCode(createActorCode(false)), - rule, - ruleName, - }); - }, + "listeners not configured": lintTest({ + code: createDispatcherCode(createActorCode(false)), + rule, + ruleName, + }), // ✅ Good - listeners configured BEFORE (chained) - "listeners before chained with property": () => { - testDenoLint({ - code: createChainedDispatcherCode( - createActorCode(true), - "setInboxListeners", - ), - rule, - ruleName, - }); - }, + "listeners before chained with property": lintTest({ + code: createChainedDispatcherCode( + createActorCode(true), + "setInboxListeners", + ), + rule, + ruleName, + }), // ✅ Good - listeners configured BEFORE (separate) - "listeners before separate with property": () => { - testDenoLint({ - code: createSeparateDispatcherCode( - createActorCode(true), - "setInboxListeners", - true, - ), - rule, - ruleName, - }); - }, + "listeners before separate with property": lintTest({ + code: createSeparateDispatcherCode( + createActorCode(true), + "setInboxListeners", + true, + ), + rule, + ruleName, + }), // ✅ Good - listeners configured AFTER (chained) - "listeners after chained with property": () => { - testDenoLint({ - code: createChainedDispatcherCode( - createActorCode(true), - "setInboxListeners", - ), - rule, - ruleName, - }); - }, + "listeners after chained with property": lintTest({ + code: createChainedDispatcherCode( + createActorCode(true), + "setInboxListeners", + ), + rule, + ruleName, + }), // ✅ Good - listeners configured AFTER (separate) - "listeners after separate with property": () => { - testDenoLint({ - code: createSeparateDispatcherCode( - createActorCode(true), - "setInboxListeners", - false, - ), - rule, - ruleName, - }); - }, + "listeners after separate with property": lintTest({ + code: createSeparateDispatcherCode( + createActorCode(true), + "setInboxListeners", + false, + ), + rule, + ruleName, + }), // ❌ Bad - listeners configured, property missing - "listeners configured property missing": () => { - testDenoLint({ - code: createChainedDispatcherCode( - createActorCode(false), - "setInboxListeners", - ), - rule, - ruleName, - expectedError, - }); - }, + "listeners configured property missing": lintTest({ + code: createChainedDispatcherCode( + createActorCode(false), + "setInboxListeners", + ), + rule, + ruleName, + expectedError, + }), // ❌ Bad - listeners before (separate), property missing - "listeners before separate property missing": () => { - testDenoLint({ - code: createSeparateDispatcherCode( - createActorCode(false), - "setInboxListeners", - true, - ), - rule, - ruleName, - expectedError, - }); - }, + "listeners before separate property missing": lintTest({ + code: createSeparateDispatcherCode( + createActorCode(false), + "setInboxListeners", + true, + ), + rule, + ruleName, + expectedError, + }), // ❌ Bad - listeners after (separate), property missing - "listeners after separate property missing": () => { - testDenoLint({ - code: createSeparateDispatcherCode( - createActorCode(false), - "setInboxListeners", - false, - ), - rule, - ruleName, - expectedError, - }); - }, + "listeners after separate property missing": lintTest({ + code: createSeparateDispatcherCode( + createActorCode(false), + "setInboxListeners", + false, + ), + rule, + ruleName, + expectedError, + }), // ❌ Bad - variable assignment without property - "variable assignment without property": () => { - testDenoLint({ - code: createChainedDispatcherCode( - `const actor = new Person({ + "variable assignment without property": lintTest({ + code: createChainedDispatcherCode( + `const actor = new Person({ id: ctx.getActorUri(identifier), name: "John Doe", }); return actor;`, - "setInboxListeners", - ), - rule, - ruleName, - expectedError, - }); - }, + "setInboxListeners", + ), + rule, + ruleName, + expectedError, + }), }; } @@ -449,100 +401,84 @@ export function createIdRequiredRuleTests(config: TestConfig) { return { // ✅ Good - non-Federation object - "non-federation object": () => { - testDenoLint({ - code: createDispatcherCode(`return new Person({ name: "John Doe" });`), - rule, - ruleName, - federationSetup: ` + "non-federation object": lintTest({ + code: createDispatcherCode(`return new Person({ name: "John Doe" });`), + rule, + ruleName, + federationSetup: ` const federation = { setActorDispatcher: () => {} }; `, - }); - }, + }), // ✅ Good - with id property (any value) - "with id property any value": () => { - testDenoLint({ - code: createDispatcherCode(`return new Person({ + "with id property any value": lintTest({ + code: createDispatcherCode(`return new Person({ id: "https://example.com/users/123", name: "John Doe", });`), - rule, - ruleName, - }); - }, + rule, + ruleName, + }), // ✅ Good - with id property using ctx.getActorUri() - "with id property using getActorUri": () => { - testDenoLint({ - code: createDispatcherCode(`return new Person({ + "with id property using getActorUri": lintTest({ + code: createDispatcherCode(`return new Person({ id: ctx.getActorUri(identifier), name: "John Doe", });`), - rule, - ruleName, - }); - }, + rule, + ruleName, + }), // ✅ Good - BlockStatement with id - "block statement with id": () => { - testDenoLint({ - code: createDispatcherCode(`const name = "John Doe"; + "block statement with id": lintTest({ + code: createDispatcherCode(`const name = "John Doe"; return new Person({ id: ctx.getActorUri(identifier), name, });`), - rule, - ruleName, - }); - }, + rule, + ruleName, + }), // ❌ Bad - without id property - "without id property": () => { - testDenoLint({ - code: createDispatcherCode(`return new Person({ name: "John Doe" });`), - rule, - ruleName, - expectedError, - }); - }, + "without id property": lintTest({ + code: createDispatcherCode(`return new Person({ name: "John Doe" });`), + rule, + ruleName, + expectedError, + }), // ❌ Bad - returning empty object - "returning empty object": () => { - testDenoLint({ - code: createDispatcherCode(`return new Person({});`), - rule, - ruleName, - expectedError, - }); - }, + "returning empty object": lintTest({ + code: createDispatcherCode(`return new Person({});`), + rule, + ruleName, + expectedError, + }), // ✅ Good - multiple properties including id - "multiple properties including id": () => { - testDenoLint({ - code: createDispatcherCode(`return new Person({ + "multiple properties including id": lintTest({ + code: createDispatcherCode(`return new Person({ id: ctx.getActorUri(identifier), name: "John Doe", inbox: ctx.getInboxUri(identifier), outbox: ctx.getOutboxUri(identifier), });`), - rule, - ruleName, - }); - }, + rule, + ruleName, + }), // ❌ Bad - variable assignment without id - "variable assignment without id": () => { - testDenoLint({ - code: createDispatcherCode( - `const actor = new Person({ name: "John Doe" }); + "variable assignment without id": lintTest({ + code: createDispatcherCode( + `const actor = new Person({ name: "John Doe" }); return actor;`, - ), - rule, - ruleName, - expectedError, - }); - }, + ), + rule, + ruleName, + expectedError, + }), }; } @@ -570,129 +506,109 @@ export function createKeyRequiredRuleTests( return { // ✅ Good - non-Federation object - "non-federation object": () => { - testDenoLint({ - code: createDispatcherCode(`return new Person({ name: "John Doe" });`), - rule, - ruleName, - federationSetup: ` + "non-federation object": lintTest({ + code: createDispatcherCode(`return new Person({ name: "John Doe" });`), + rule, + ruleName, + federationSetup: ` const federation = { setActorDispatcher: () => {} }; `, - }); - }, + }), // ✅ Good - key pairs dispatcher NOT configured - "key pairs dispatcher not configured": () => { - testDenoLint({ - code: createDispatcherCode(createActorWithKey(false)), - rule, - ruleName, - }); - }, + "key pairs dispatcher not configured": lintTest({ + code: createDispatcherCode(createActorWithKey(false)), + rule, + ruleName, + }), // ✅ Good - key pairs dispatcher configured BEFORE (separate) - "key pairs before separate with property": () => { - testDenoLint({ - code: createSeparateDispatcherCode( - createActorWithKey(true), - "setKeyPairsDispatcher", - true, - ), - rule, - ruleName, - }); - }, + "key pairs before separate with property": lintTest({ + code: createSeparateDispatcherCode( + createActorWithKey(true), + "setKeyPairsDispatcher", + true, + ), + rule, + ruleName, + }), // ✅ Good - key pairs dispatcher configured BEFORE (chained) - "key pairs before chained with property": () => { - testDenoLint({ - code: createChainedDispatcherCode( - createActorWithKey(true), - "setKeyPairsDispatcher", - ), - rule, - ruleName, - }); - }, + "key pairs before chained with property": lintTest({ + code: createChainedDispatcherCode( + createActorWithKey(true), + "setKeyPairsDispatcher", + ), + rule, + ruleName, + }), // ✅ Good - key pairs dispatcher configured AFTER (separate) - "key pairs after separate with property": () => { - testDenoLint({ - code: createSeparateDispatcherCode( - createActorWithKey(true), - "setKeyPairsDispatcher", - false, - ), - rule, - ruleName, - }); - }, + "key pairs after separate with property": lintTest({ + code: createSeparateDispatcherCode( + createActorWithKey(true), + "setKeyPairsDispatcher", + false, + ), + rule, + ruleName, + }), // ✅ Good - key pairs dispatcher configured AFTER (chained) - "key pairs after chained with property": () => { - testDenoLint({ - code: createChainedDispatcherCode( - createActorWithKey(true), - "setKeyPairsDispatcher", - ), - rule, - ruleName, - }); - }, + "key pairs after chained with property": lintTest({ + code: createChainedDispatcherCode( + createActorWithKey(true), + "setKeyPairsDispatcher", + ), + rule, + ruleName, + }), // ❌ Bad - key pairs dispatcher configured BEFORE (separate), property missing - "key pairs before separate property missing": () => { - testDenoLint({ - code: createSeparateDispatcherCode( - createActorWithKey(false), - "setKeyPairsDispatcher", - true, - ), - rule, - ruleName, - expectedError, - }); - }, + "key pairs before separate property missing": lintTest({ + code: createSeparateDispatcherCode( + createActorWithKey(false), + "setKeyPairsDispatcher", + true, + ), + rule, + ruleName, + expectedError, + }), // ❌ Bad - key pairs dispatcher configured BEFORE (chained), property missing - "key pairs before chained property missing": () => { - testDenoLint({ - code: createChainedDispatcherCode( - createActorWithKey(false), - "setKeyPairsDispatcher", - ), - rule, - ruleName, - expectedError, - }); - }, + "key pairs before chained property missing": lintTest({ + code: createChainedDispatcherCode( + createActorWithKey(false), + "setKeyPairsDispatcher", + ), + rule, + ruleName, + expectedError, + }), // ❌ Bad - key pairs dispatcher configured AFTER (separate), property missing - "key pairs after separate property missing": () => { - testDenoLint({ - code: createSeparateDispatcherCode( - createActorWithKey(false), - "setKeyPairsDispatcher", - false, - ), - rule, - ruleName, - expectedError, - }); - }, + "key pairs after separate property missing": lintTest({ + code: createSeparateDispatcherCode( + createActorWithKey(false), + "setKeyPairsDispatcher", + false, + ), + rule, + ruleName, + expectedError, + }), // ❌ Bad - key pairs dispatcher configured AFTER (chained), property missing - "key pairs after chained property missing": () => { - testDenoLint({ - code: createChainedDispatcherCode( - createActorWithKey(false), - "setKeyPairsDispatcher", - ), - rule, - ruleName, - expectedError, - }); - }, + "key pairs after chained property missing": lintTest({ + code: createChainedDispatcherCode( + createActorWithKey(false), + "setKeyPairsDispatcher", + ), + rule, + ruleName, + expectedError, + }), }; } @@ -731,64 +647,51 @@ export function createMismatchRuleTests( return { // ✅ Good - non-Federation object - "non-federation object": () => { - testDenoLint({ - code: createDispatcherCode(createActorCode(wrongGetter)), - rule, - ruleName, - federationSetup: ` + "non-federation object": lintTest({ + code: createDispatcherCode(createActorCode(wrongGetter)), + rule, + ruleName, + federationSetup: ` const federation = { setActorDispatcher: () => {} }; `, - }); - }, + }), // ✅ Good - correct getter used - "correct getter used": () => { - testDenoLint({ - code: createDispatcherCode(createActorCode(prop.getter)), - rule, - ruleName, - }); - }, + "correct getter used": lintTest({ + code: createDispatcherCode(createActorCode(prop.getter)), + rule, + ruleName, + }), // ❌ Bad - wrong getter used - "wrong getter used": () => { - testDenoLint({ - code: createDispatcherCode(createActorCode(wrongGetter)), - rule, - ruleName, - expectedError, - }); - }, + "wrong getter used": lintTest({ + code: createDispatcherCode(createActorCode(wrongGetter)), + rule, + ruleName, + expectedError, + }), // ❌ Bad - wrong identifier - "wrong identifier": () => { - const propCode = createPropertyAssignment(prop, { - ctxName: "wrongContext", - }); - testDenoLint({ - code: createDispatcherCode(`return new Person({ + "wrong identifier": lintTest({ + code: createDispatcherCode(`return new Person({ id: ctx.getActorUri(identifier), - ${propCode} + ${createPropertyAssignment(prop, { ctxName: "wrongContext" })} name: "John Doe", });`), - rule, - ruleName, - expectedError, - }); - }, + rule, + ruleName, + expectedError, + }), // ✅ Good - property not present (no error) - "property not present": () => { - testDenoLint({ - code: createDispatcherCode(`return new Person({ + "property not present": lintTest({ + code: createDispatcherCode(`return new Person({ id: ctx.getActorUri(identifier), name: "John Doe", });`), - rule, - ruleName, - }); - }, + rule, + ruleName, + }), }; } @@ -803,87 +706,73 @@ export function createIdMismatchRuleTests(config: TestConfig) { return { // ✅ Good - non-Federation object - "non-federation object": () => { - testDenoLint({ - code: createDispatcherCode( - `return new Person({ id: ctx.getFollowingUri(identifier) });`, - ), - rule, - ruleName, - federationSetup: ` + "non-federation object": lintTest({ + code: createDispatcherCode( + `return new Person({ id: ctx.getFollowingUri(identifier) });`, + ), + rule, + ruleName, + federationSetup: ` const federation = { setActorDispatcher: () => {} }; `, - }); - }, + }), // ✅ Good - correct getter used - "correct getter used": () => { - testDenoLint({ - code: createDispatcherCode( - `return new Person({ id: ctx.getActorUri(identifier) });`, - ), - rule, - ruleName, - }); - }, + "correct getter used": lintTest({ + code: createDispatcherCode( + `return new Person({ id: ctx.getActorUri(identifier) });`, + ), + rule, + ruleName, + }), // ✅ Good - literal string id - "literal string id": () => { - testDenoLint({ - code: createDispatcherCode( - `return new Person({ id: "https://example.com/users/123" });`, - ), - rule, - ruleName, - }); - }, + "literal string id": lintTest({ + code: createDispatcherCode( + `return new Person({ id: "https://example.com/users/123" });`, + ), + rule, + ruleName, + }), // ✅ Good - new URL as id - "new URL as id": () => { - testDenoLint({ - code: createDispatcherCode( - `return new Person({ id: new URL("https://example.com/users/123") });`, - ), - rule, - ruleName, - }); - }, + "new URL as id": lintTest({ + code: createDispatcherCode( + `return new Person({ id: new URL("https://example.com/users/123") });`, + ), + rule, + ruleName, + }), // ❌ Bad - wrong getter used - "wrong getter used": () => { - testDenoLint({ - code: createDispatcherCode( - `return new Person({ id: ctx.getFollowingUri(identifier) });`, - ), - rule, - ruleName, - expectedError, - }); - }, + "wrong getter used": lintTest({ + code: createDispatcherCode( + `return new Person({ id: ctx.getFollowingUri(identifier) });`, + ), + rule, + ruleName, + expectedError, + }), // ❌ Bad - wrong method in object literal - "wrong method in object literal": () => { - testDenoLint({ - code: createDispatcherCode( - `return { id: ctx.getInboxUri(identifier) };`, - ), - rule, - ruleName, - expectedError, - }); - }, + "wrong method in object literal": lintTest({ + code: createDispatcherCode( + `return { id: ctx.getInboxUri(identifier) };`, + ), + rule, + ruleName, + expectedError, + }), // ❌ Bad - wrong identifier - "wrong identifier": () => { - testDenoLint({ - code: createDispatcherCode( - `return new Person({ id: wrongContext.getActorUri(identifier) });`, - ), - rule, - ruleName, - expectedError, - }); - }, + "wrong identifier": lintTest({ + code: createDispatcherCode( + `return new Person({ id: wrongContext.getActorUri(identifier) });`, + ), + rule, + ruleName, + expectedError, + }), }; } @@ -904,86 +793,73 @@ export function createRequiredEdgeCaseTests( const expectedError = actorPropertyRequired(prop); const createLocalPropertyCode = () => createPropertyAssignment(prop); + const propCode = createLocalPropertyCode(); return { // ✅ Ternary with property in both branches - "ternary with property in both branches": () => { - const propCode = createLocalPropertyCode(); - testDenoLint({ - code: createChainedDispatcherCode( - `return condition + "ternary with property in both branches": lintTest({ + code: createChainedDispatcherCode( + `return condition ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" });`, - dispatcherMethod, - ), - rule, - ruleName, - }); - }, + dispatcherMethod, + ), + rule, + ruleName, + }), // ❌ Ternary missing property in consequent - "ternary missing property in consequent": () => { - const propCode = createLocalPropertyCode(); - testDenoLint({ - code: createChainedDispatcherCode( - `return condition + "ternary missing property in consequent": lintTest({ + code: createChainedDispatcherCode( + `return condition ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" });`, - dispatcherMethod, - ), - rule, - ruleName, - expectedError, - }); - }, + dispatcherMethod, + ), + rule, + ruleName, + expectedError, + }), // ❌ Ternary missing property in alternate - "ternary missing property in alternate": () => { - const propCode = createLocalPropertyCode(); - testDenoLint({ - code: createChainedDispatcherCode( - `return condition + "ternary missing property in alternate": lintTest({ + code: createChainedDispatcherCode( + `return condition ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) : new Person({ id: ctx.getActorUri(identifier), name: "B" });`, - dispatcherMethod, - ), - rule, - ruleName, - expectedError, - }); - }, + dispatcherMethod, + ), + rule, + ruleName, + expectedError, + }), // ❌ Ternary missing property in both - "ternary missing property in both branches": () => { - testDenoLint({ - code: createChainedDispatcherCode( - `return condition + "ternary missing property in both branches": lintTest({ + code: createChainedDispatcherCode( + `return condition ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) : new Person({ id: ctx.getActorUri(identifier), name: "B" });`, - dispatcherMethod, - ), - rule, - ruleName, - expectedError, - }); - }, + dispatcherMethod, + ), + rule, + ruleName, + expectedError, + }), // ✅ Nested ternary with property - "nested ternary with property": () => { - const propCode = createLocalPropertyCode(); - testDenoLint({ - code: createChainedDispatcherCode( - `return condition1 + "nested ternary with property": lintTest({ + code: createChainedDispatcherCode( + `return condition1 ? (condition2 ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" })) : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" });`, - dispatcherMethod, - ), - rule, - ruleName, - }); - }, + dispatcherMethod, + ), + rule, + ruleName, + }), }; } @@ -1008,84 +884,68 @@ export function createMismatchEdgeCaseTests( const createLocalPropertyCode = (getter: string) => createPropertyAssignment(prop, { getter }); - + const propCode = createLocalPropertyCode(prop.getter); + const wrongPropCode = createLocalPropertyCode(wrongGetter); return { // ✅ Ternary with correct getter in both branches - "ternary with correct getter in both branches": () => { - const propCode = createLocalPropertyCode(prop.getter); - testDenoLint({ - code: createDispatcherCode( - `return condition + "ternary with correct getter in both branches": lintTest({ + code: createDispatcherCode( + `return condition ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" });`, - ), - rule, - ruleName, - }); - }, + ), + rule, + ruleName, + }), // ❌ Ternary with wrong getter in consequent - "ternary with wrong getter in consequent": () => { - const correctPropCode = createLocalPropertyCode(prop.getter); - const wrongPropCode = createLocalPropertyCode(wrongGetter); - testDenoLint({ - code: createDispatcherCode( - `return condition + "ternary with wrong getter in consequent": lintTest({ + code: createDispatcherCode( + `return condition ? new Person({ id: ctx.getActorUri(identifier), ${wrongPropCode} name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), ${correctPropCode} name: "B" });`, - ), - rule, - ruleName, - expectedError, - }); - }, + : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" });`, + ), + rule, + ruleName, + expectedError, + }), // ❌ Ternary with wrong getter in alternate - "ternary with wrong getter in alternate": () => { - const correctPropCode = createLocalPropertyCode(prop.getter); - const wrongPropCode = createLocalPropertyCode(wrongGetter); - testDenoLint({ - code: createDispatcherCode( - `return condition - ? new Person({ id: ctx.getActorUri(identifier), ${correctPropCode} name: "A" }) + "ternary with wrong getter in alternate": lintTest({ + code: createDispatcherCode( + `return condition + ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) : new Person({ id: ctx.getActorUri(identifier), ${wrongPropCode} name: "B" });`, - ), - rule, - ruleName, - expectedError, - }); - }, + ), + rule, + ruleName, + expectedError, + }), // ❌ Ternary with wrong getter in both - "ternary with wrong getter in both branches": () => { - const wrongPropCode = createLocalPropertyCode(wrongGetter); - testDenoLint({ - code: createDispatcherCode( - `return condition + "ternary with wrong getter in both branches": lintTest({ + code: createDispatcherCode( + `return condition ? new Person({ id: ctx.getActorUri(identifier), ${wrongPropCode} name: "A" }) : new Person({ id: ctx.getActorUri(identifier), ${wrongPropCode} name: "B" });`, - ), - rule, - ruleName, - expectedError, - }); - }, + ), + rule, + ruleName, + expectedError, + }), // ✅ Nested ternary with correct getter - "nested ternary with correct getter": () => { - const propCode = createLocalPropertyCode(prop.getter); - testDenoLint({ - code: createDispatcherCode( - `return condition1 + "nested ternary with correct getter": lintTest({ + code: createDispatcherCode( + `return condition1 ? (condition2 ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" })) : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" });`, - ), - rule, - ruleName, - }); - }, + ), + rule, + ruleName, + }), }; } @@ -1098,74 +958,64 @@ export function createIdRequiredEdgeCaseTests(config: TestConfig) { return { // ✅ Ternary with id in both branches - "ternary with id in both branches": () => { - testDenoLint({ - code: createDispatcherCode( - `return condition + "ternary with id in both branches": lintTest({ + code: createDispatcherCode( + `return condition ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) : new Person({ id: ctx.getActorUri(identifier), name: "B" });`, - ), - rule, - ruleName, - }); - }, + ), + rule, + ruleName, + }), // ❌ Ternary missing id in consequent - "ternary missing id in consequent": () => { - testDenoLint({ - code: createDispatcherCode( - `return condition + "ternary missing id in consequent": lintTest({ + code: createDispatcherCode( + `return condition ? new Person({ name: "A" }) : new Person({ id: ctx.getActorUri(identifier), name: "B" });`, - ), - rule, - ruleName, - expectedError, - }); - }, + ), + rule, + ruleName, + expectedError, + }), // ❌ Ternary missing id in alternate - "ternary missing id in alternate": () => { - testDenoLint({ - code: createDispatcherCode( - `return condition + "ternary missing id in alternate": lintTest({ + code: createDispatcherCode( + `return condition ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) : new Person({ name: "B" });`, - ), - rule, - ruleName, - expectedError, - }); - }, + ), + rule, + ruleName, + expectedError, + }), // ❌ Ternary missing id in both - "ternary missing id in both branches": () => { - testDenoLint({ - code: createDispatcherCode( - `return condition + "ternary missing id in both branches": lintTest({ + code: createDispatcherCode( + `return condition ? new Person({ name: "A" }) : new Person({ name: "B" });`, - ), - rule, - ruleName, - expectedError, - }); - }, + ), + rule, + ruleName, + expectedError, + }), // ✅ Nested ternary with id - "nested ternary with id": () => { - testDenoLint({ - code: createDispatcherCode( - `return condition1 + "nested ternary with id": lintTest({ + code: createDispatcherCode( + `return condition1 ? (condition2 ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) : new Person({ id: ctx.getActorUri(identifier), name: "B" })) : new Person({ id: ctx.getActorUri(identifier), name: "C" });`, - ), - rule, - ruleName, - }); - }, + ), + rule, + ruleName, + }), }; } @@ -1180,74 +1030,64 @@ export function createIdMismatchEdgeCaseTests(config: TestConfig) { return { // ✅ Ternary with correct getter in both branches - "ternary with correct getter in both branches": () => { - testDenoLint({ - code: createDispatcherCode( - `return condition + "ternary with correct getter in both branches": lintTest({ + code: createDispatcherCode( + `return condition ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) : new Person({ id: ctx.getActorUri(identifier), name: "B" });`, - ), - rule, - ruleName, - }); - }, + ), + rule, + ruleName, + }), // ❌ Ternary with wrong getter in consequent - "ternary with wrong getter in consequent": () => { - testDenoLint({ - code: createDispatcherCode( - `return condition + "ternary with wrong getter in consequent": lintTest({ + code: createDispatcherCode( + `return condition ? new Person({ id: ctx.getFollowingUri(identifier), name: "A" }) : new Person({ id: ctx.getActorUri(identifier), name: "B" });`, - ), - rule, - ruleName, - expectedError, - }); - }, + ), + rule, + ruleName, + expectedError, + }), // ❌ Ternary with wrong getter in alternate - "ternary with wrong getter in alternate": () => { - testDenoLint({ - code: createDispatcherCode( - `return condition + "ternary with wrong getter in alternate": lintTest({ + code: createDispatcherCode( + `return condition ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) : new Person({ id: ctx.getFollowingUri(identifier), name: "B" });`, - ), - rule, - ruleName, - expectedError, - }); - }, + ), + rule, + ruleName, + expectedError, + }), // ❌ Ternary with wrong getter in both - "ternary with wrong getter in both branches": () => { - testDenoLint({ - code: createDispatcherCode( - `return condition + "ternary with wrong getter in both branches": lintTest({ + code: createDispatcherCode( + `return condition ? new Person({ id: ctx.getFollowingUri(identifier), name: "A" }) : new Person({ id: ctx.getFollowingUri(identifier), name: "B" });`, - ), - rule, - ruleName, - expectedError, - }); - }, + ), + rule, + ruleName, + expectedError, + }), // ✅ Nested ternary with correct getter - "nested ternary with correct getter": () => { - testDenoLint({ - code: createDispatcherCode( - `return condition1 + "nested ternary with correct getter": lintTest({ + code: createDispatcherCode( + `return condition1 ? (condition2 ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) : new Person({ id: ctx.getActorUri(identifier), name: "B" })) : new Person({ id: ctx.getActorUri(identifier), name: "C" });`, - ), - rule, - ruleName, - }); - }, + ), + rule, + ruleName, + }), }; } @@ -1264,103 +1104,72 @@ export function createKeyRequiredEdgeCaseTests( const createPropertyCode = () => `${propertyName}: ctx.getActorKeyPairs(identifier),`; + const propCode = createPropertyCode(); return { // ✅ Ternary with property in both branches - "ternary with property in both branches": () => { - const propCode = createPropertyCode(); - testDenoLint({ - code: createChainedDispatcherCode( - `return condition - ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" });`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - }); - }, + "ternary with property in both branches": lintTest({ + code: createChainedDispatcherCode( + `return condition + ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" });`, + "setKeyPairsDispatcher", + ), + rule, + ruleName, + }), // ❌ Ternary missing property in consequent - "ternary missing property in consequent": () => { - const propCode = createPropertyCode(); - testDenoLint({ - code: createChainedDispatcherCode( - `return condition - ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" });`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - expectedError, - }); - }, + "ternary missing property in consequent": lintTest({ + code: createChainedDispatcherCode( + `return condition + ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" });`, + "setKeyPairsDispatcher", + ), + rule, + ruleName, + expectedError, + }), // ❌ Ternary missing property in alternate - "ternary missing property in alternate": () => { - const propCode = createPropertyCode(); - testDenoLint({ - code: createChainedDispatcherCode( - `return condition + "ternary missing property in alternate": lintTest({ + code: createChainedDispatcherCode( + `return condition ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) : new Person({ id: ctx.getActorUri(identifier), name: "B" });`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - expectedError, - }); - }, + "setKeyPairsDispatcher", + ), + rule, + ruleName, + expectedError, + }), // ❌ Ternary missing property in both - "ternary missing property in both branches": () => { - testDenoLint({ - code: createChainedDispatcherCode( - `return condition + "ternary missing property in both branches": lintTest({ + code: createChainedDispatcherCode( + `return condition ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) : new Person({ id: ctx.getActorUri(identifier), name: "B" });`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - expectedError, - }); - }, + "setKeyPairsDispatcher", + ), + rule, + ruleName, + expectedError, + }), // ✅ Nested ternary with property - "nested ternary with property": () => { - const propCode = createPropertyCode(); - testDenoLint({ - code: createChainedDispatcherCode( - `return condition1 + "nested ternary with property": lintTest({ + code: createChainedDispatcherCode( + `return condition1 ? (condition2 ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" })) : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" });`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - }); - }, + "setKeyPairsDispatcher", + ), + rule, + ruleName, + }), }; } - -// ============================================================================= -// Test Runner Utilities -// ============================================================================= - -/** - * Runs all tests from a test object - */ -export function runTests( - tests: Record void>, - ruleName: string, - prefix: string, -) { - const { test } = require("node:test"); - for (const [name, testFn] of Object.entries(tests)) { - test(`${ruleName}: ${prefix} - ${name}`, testFn); - } -} diff --git a/packages/lint/src/lib/test.ts b/packages/lint/src/lib/test.ts index d7be09b6a..fb82ffc85 100644 --- a/packages/lint/src/lib/test.ts +++ b/packages/lint/src/lib/test.ts @@ -1,10 +1,12 @@ import { isNil } from "@fxts/core"; -import { assert, assertEquals, assertGreater } from "jsr:@std/assert"; +import { RuleTester } from "@typescript-eslint/rule-tester"; +import type { TSESLint } from "@typescript-eslint/utils"; +import assert from "node:assert/strict"; import { FEDERATION_SETUP } from "./const.ts"; const LINT_PLUGIN_NAME = "fedify-lint-test"; -export function testDenoLint( +function testDenoLint( { code, rule, @@ -15,7 +17,7 @@ export function testDenoLint( code: string; rule: Deno.lint.Rule; ruleName: string; - federationSetup?: string | false; + federationSetup?: string; expectedError?: string; }, ) { @@ -27,28 +29,122 @@ export function testDenoLint( const diagnostics = Deno.lint.runPlugin( plugin, ruleName + ".test.ts", - `${federationSetup ? federationSetup : ""}\n\n${code}`, + `${federationSetup ?? federationSetup}\n\n${code}`, ); if (isNil(expectedError)) { - assertEquals( + assert.equal( diagnostics.length, 0, - "Should not report issues when id property is present", + `Should not report issues when id property is present but found: \n${ + diagnostics.map((d) => " - " + d.message).join(", ") + } + +=== CODE === +${code} +=======`, ); } else { - assertGreater( - diagnostics.length, - 0, + assert.ok( + diagnostics.length > 0, "Expected at least one diagnostic error but found none", ); const lintId = `${LINT_PLUGIN_NAME}/${ruleName}`; const matched = diagnostics.some((diag) => diag.message.includes(expectedError) ); - assert( + assert.ok( matched, `Expected ${lintId} to report but it did not.`, ); } } + +RuleTester.afterAll = () => {}; +RuleTester.describe = () => {}; + +function testEslintRule( + { + code, + rule, + ruleName, + federationSetup = FEDERATION_SETUP, + expectedError, + }: { + code: string; + rule: TSESLint.RuleModule; + ruleName: string; + federationSetup?: string; + expectedError?: string; + }, +) { + const ruleTester = new RuleTester({ + languageOptions: { + ecmaVersion: "latest", + sourceType: "module", + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + }); + + const fullCode = `${federationSetup ?? federationSetup}\n\n${code}`; + + if (isNil(expectedError)) { + ruleTester.run(ruleName, rule, { + valid: [fullCode], + invalid: [], + }); + } else { + ruleTester.run(ruleName, rule, { + valid: [], + invalid: [ + { + code: fullCode, + errors: [{ messageId: expectedError }], + }, + ], + }); + } +} + +export default function lintTest( + { + code, + rule: { deno, eslint }, + ruleName, + federationSetup = FEDERATION_SETUP, + expectedError, + }: { + code: string; + rule: { + deno: Deno.lint.Rule; + eslint: TSESLint.RuleModule; + }; + ruleName: string; + federationSetup?: string; + expectedError?: string; + }, +) { + if ("Deno" in globalThis) { + return () => + testDenoLint({ + code, + rule: deno, + ruleName, + federationSetup, + expectedError, + }); + } else { + return () => + testEslintRule({ + code, + rule: eslint, + ruleName, + federationSetup, + expectedError, + }); + } +} diff --git a/packages/lint/src/tests/actor-assertion-method-required.test.ts b/packages/lint/src/tests/actor-assertion-method-required.test.ts index 3d13ad642..474e0289a 100644 --- a/packages/lint/src/tests/actor-assertion-method-required.test.ts +++ b/packages/lint/src/tests/actor-assertion-method-required.test.ts @@ -1,14 +1,14 @@ import { test } from "node:test"; -import { properties } from "../lib/const.ts"; +import { properties, RULE_IDS } from "../lib/const.ts"; import { actorPropertyRequired } from "../lib/messages.ts"; -import { testDenoLint } from "../lib/test.ts"; -import { - ACTOR_ASSERTION_METHOD_REQUIRED as ruleName, - default as rule, -} from "../rules/actor-assertion-method-required.ts"; +import lintTest from "../lib/test.ts"; +import * as rule from "../rules/actor-assertion-method-required.ts"; -test(`${ruleName}: ✅ Good - \`setActorDispatcher\` called on non-Federation object`, () => { - testDenoLint({ +const ruleName = RULE_IDS.actorAssertionMethodRequired; + +test( + `${ruleName}: ✅ Good - \`setActorDispatcher\` called on non-Federation object`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { return new Person({ @@ -23,11 +23,12 @@ test(`${ruleName}: ✅ Good - \`setActorDispatcher\` called on non-Federation ob setActorDispatcher: () => {} }; `, - }); -}); + }), +); -test(`${ruleName}: ✅ Good - key pairs dispatcher NOT configured, property missing (no error)`, () => { - testDenoLint({ +test( + `${ruleName}: ✅ Good - key pairs dispatcher NOT configured, property missing (no error)`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { return new Person({ @@ -38,11 +39,12 @@ test(`${ruleName}: ✅ Good - key pairs dispatcher NOT configured, property miss `, rule, ruleName, - }); -}); + }), +); -test(`${ruleName}: ✅ Good - key pairs dispatcher configured BEFORE setActorDispatcher, property present`, () => { - testDenoLint({ +test( + `${ruleName}: ✅ Good - key pairs dispatcher configured BEFORE setActorDispatcher, property present`, + lintTest({ code: ` federation.setKeyPairsDispatcher(async (ctx, identifier) => { return []; @@ -58,11 +60,12 @@ test(`${ruleName}: ✅ Good - key pairs dispatcher configured BEFORE setActorDis `, rule, ruleName, - }); -}); + }), +); -test(`${ruleName}: ✅ Good - key pairs dispatcher configured BEFORE setActorDispatcher (chained), property present`, () => { - testDenoLint({ +test( + `${ruleName}: ✅ Good - key pairs dispatcher configured BEFORE setActorDispatcher (chained), property present`, + lintTest({ code: ` federation .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { @@ -76,11 +79,12 @@ test(`${ruleName}: ✅ Good - key pairs dispatcher configured BEFORE setActorDis `, rule, ruleName, - }); -}); + }), +); -test(`${ruleName}: ✅ Good - key pairs dispatcher configured AFTER setActorDispatcher, property present`, () => { - testDenoLint({ +test( + `${ruleName}: ✅ Good - key pairs dispatcher configured AFTER setActorDispatcher, property present`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { return new Person({ @@ -96,11 +100,12 @@ test(`${ruleName}: ✅ Good - key pairs dispatcher configured AFTER setActorDisp `, rule, ruleName, - }); -}); + }), +); -test(`${ruleName}: ✅ Good - key pairs dispatcher configured AFTER setActorDispatcher (chained), property present`, () => { - testDenoLint({ +test( + `${ruleName}: ✅ Good - key pairs dispatcher configured AFTER setActorDispatcher (chained), property present`, + lintTest({ code: ` federation .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { @@ -114,11 +119,12 @@ test(`${ruleName}: ✅ Good - key pairs dispatcher configured AFTER setActorDisp `, rule, ruleName, - }); -}); + }), +); -test(`${ruleName}: ❌ Bad - key pairs dispatcher configured BEFORE, property missing`, () => { - testDenoLint({ +test( + `${ruleName}: ❌ Bad - key pairs dispatcher configured BEFORE, property missing`, + lintTest({ code: ` federation.setKeyPairsDispatcher(async (ctx, identifier) => { return []; @@ -134,11 +140,12 @@ test(`${ruleName}: ❌ Bad - key pairs dispatcher configured BEFORE, property mi rule, ruleName, expectedError: actorPropertyRequired(properties.assertionMethod), - }); -}); + }), +); -test(`${ruleName}: ❌ Bad - key pairs dispatcher configured BEFORE (chained), property missing`, () => { - testDenoLint({ +test( + `${ruleName}: ❌ Bad - key pairs dispatcher configured BEFORE (chained), property missing`, + lintTest({ code: ` federation .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { @@ -152,11 +159,12 @@ test(`${ruleName}: ❌ Bad - key pairs dispatcher configured BEFORE (chained), p rule, ruleName, expectedError: actorPropertyRequired(properties.assertionMethod), - }); -}); + }), +); -test(`${ruleName}: ❌ Bad - key pairs dispatcher configured AFTER, property missing`, () => { - testDenoLint({ +test( + `${ruleName}: ❌ Bad - key pairs dispatcher configured AFTER, property missing`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { return new Person({ @@ -172,11 +180,12 @@ test(`${ruleName}: ❌ Bad - key pairs dispatcher configured AFTER, property mis rule, ruleName, expectedError: actorPropertyRequired(properties.assertionMethod), - }); -}); + }), +); -test(`${ruleName}: ❌ Bad - key pairs dispatcher configured AFTER (chained), property missing`, () => { - testDenoLint({ +test( + `${ruleName}: ❌ Bad - key pairs dispatcher configured AFTER (chained), property missing`, + lintTest({ code: ` federation .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { @@ -190,5 +199,5 @@ test(`${ruleName}: ❌ Bad - key pairs dispatcher configured AFTER (chained), pr rule, ruleName, expectedError: actorPropertyRequired(properties.assertionMethod), - }); -}); + }), +); diff --git a/packages/lint/src/tests/actor-featured-property-mismatch.test.ts b/packages/lint/src/tests/actor-featured-property-mismatch.test.ts index 20d112d82..ae04a936e 100644 --- a/packages/lint/src/tests/actor-featured-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-featured-property-mismatch.test.ts @@ -1,12 +1,12 @@ import { test } from "node:test"; +import { RULE_IDS } from "../lib/const.ts"; import { createMismatchEdgeCaseTests, createMismatchRuleTests, } from "../lib/test-templates.ts"; -import { - ACTOR_FEATURED_PROPERTY_MISMATCH as ruleName, - default as rule, -} from "../rules/actor-featured-property-mismatch.ts"; +import * as rule from "../rules/actor-featured-property-mismatch.ts"; + +const ruleName = RULE_IDS.actorFeaturedPropertyMismatch; const config = { rule, ruleName }; diff --git a/packages/lint/src/tests/actor-featured-property-required.test.ts b/packages/lint/src/tests/actor-featured-property-required.test.ts index eeb79ebc8..e1a522010 100644 --- a/packages/lint/src/tests/actor-featured-property-required.test.ts +++ b/packages/lint/src/tests/actor-featured-property-required.test.ts @@ -1,12 +1,12 @@ import { test } from "node:test"; +import { RULE_IDS } from "../lib/const.ts"; import { createRequiredDispatcherRuleTests, createRequiredEdgeCaseTests, } from "../lib/test-templates.ts"; -import { - ACTOR_FEATURED_PROPERTY_REQUIRED as ruleName, - default as rule, -} from "../rules/actor-featured-property-required.ts"; +import * as rule from "../rules/actor-featured-property-required.ts"; + +const ruleName = RULE_IDS.actorFeaturedPropertyRequired; const config = { rule, ruleName }; diff --git a/packages/lint/src/tests/actor-featured-tags-property-mismatch.test.ts b/packages/lint/src/tests/actor-featured-tags-property-mismatch.test.ts index fe31d857d..a60fc2f6c 100644 --- a/packages/lint/src/tests/actor-featured-tags-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-featured-tags-property-mismatch.test.ts @@ -1,12 +1,12 @@ import { test } from "node:test"; +import { RULE_IDS } from "../lib/const.ts"; import { createMismatchEdgeCaseTests, createMismatchRuleTests, } from "../lib/test-templates.ts"; -import { - ACTOR_FEATURED_TAGS_PROPERTY_MISMATCH as ruleName, - default as rule, -} from "../rules/actor-featured-tags-property-mismatch.ts"; +import * as rule from "../rules/actor-featured-tags-property-mismatch.ts"; + +const ruleName = RULE_IDS.actorFeaturedTagsPropertyMismatch; const config = { rule, ruleName }; diff --git a/packages/lint/src/tests/actor-featured-tags-property-required.test.ts b/packages/lint/src/tests/actor-featured-tags-property-required.test.ts index 79acb25fe..7f524ae26 100644 --- a/packages/lint/src/tests/actor-featured-tags-property-required.test.ts +++ b/packages/lint/src/tests/actor-featured-tags-property-required.test.ts @@ -1,12 +1,12 @@ import { test } from "node:test"; +import { RULE_IDS } from "../lib/const.ts"; import { createRequiredDispatcherRuleTests, createRequiredEdgeCaseTests, } from "../lib/test-templates.ts"; -import { - ACTOR_FEATURED_TAGS_PROPERTY_REQUIRED as ruleName, - default as rule, -} from "../rules/actor-featured-tags-property-required.ts"; +import * as rule from "../rules/actor-featured-tags-property-required.ts"; + +const ruleName = RULE_IDS.actorFeaturedTagsPropertyRequired; const config = { rule, ruleName }; diff --git a/packages/lint/src/tests/actor-followers-property-mismatch.test.ts b/packages/lint/src/tests/actor-followers-property-mismatch.test.ts index 9a2eea06a..04fe04cf7 100644 --- a/packages/lint/src/tests/actor-followers-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-followers-property-mismatch.test.ts @@ -1,12 +1,12 @@ import { test } from "node:test"; +import { RULE_IDS } from "../lib/const.ts"; import { createMismatchEdgeCaseTests, createMismatchRuleTests, } from "../lib/test-templates.ts"; -import { - ACTOR_FOLLOWERS_PROPERTY_MISMATCH as ruleName, - default as rule, -} from "../rules/actor-followers-property-mismatch.ts"; +import * as rule from "../rules/actor-followers-property-mismatch.ts"; + +const ruleName = RULE_IDS.actorFollowersPropertyMismatch; const config = { rule, ruleName }; diff --git a/packages/lint/src/tests/actor-followers-property-required.test.ts b/packages/lint/src/tests/actor-followers-property-required.test.ts index a8c5f6a98..87cf0030f 100644 --- a/packages/lint/src/tests/actor-followers-property-required.test.ts +++ b/packages/lint/src/tests/actor-followers-property-required.test.ts @@ -1,12 +1,12 @@ import { test } from "node:test"; +import { RULE_IDS } from "../lib/const.ts"; import { createRequiredDispatcherRuleTests, createRequiredEdgeCaseTests, } from "../lib/test-templates.ts"; -import { - ACTOR_FOLLOWERS_PROPERTY_REQUIRED as ruleName, - default as rule, -} from "../rules/actor-followers-property-required.ts"; +import * as rule from "../rules/actor-followers-property-required.ts"; + +const ruleName = RULE_IDS.actorFollowersPropertyRequired; const config = { rule, ruleName }; diff --git a/packages/lint/src/tests/actor-following-property-mismatch.test.ts b/packages/lint/src/tests/actor-following-property-mismatch.test.ts index b741156b9..a7d5e4cfc 100644 --- a/packages/lint/src/tests/actor-following-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-following-property-mismatch.test.ts @@ -1,12 +1,12 @@ import { test } from "node:test"; +import { RULE_IDS } from "../lib/const.ts"; import { createMismatchEdgeCaseTests, createMismatchRuleTests, } from "../lib/test-templates.ts"; -import { - ACTOR_FOLLOWING_PROPERTY_MISMATCH as ruleName, - default as rule, -} from "../rules/actor-following-property-mismatch.ts"; +import * as rule from "../rules/actor-following-property-mismatch.ts"; + +const ruleName = RULE_IDS.actorFollowingPropertyMismatch; const config = { rule, ruleName }; diff --git a/packages/lint/src/tests/actor-following-property-required.test.ts b/packages/lint/src/tests/actor-following-property-required.test.ts index 0c9c2ea91..a49483578 100644 --- a/packages/lint/src/tests/actor-following-property-required.test.ts +++ b/packages/lint/src/tests/actor-following-property-required.test.ts @@ -1,12 +1,12 @@ import { test } from "node:test"; +import { RULE_IDS } from "../lib/const.ts"; import { createRequiredDispatcherRuleTests, createRequiredEdgeCaseTests, } from "../lib/test-templates.ts"; -import { - ACTOR_FOLLOWING_PROPERTY_REQUIRED as ruleName, - default as rule, -} from "../rules/actor-following-property-required.ts"; +import * as rule from "../rules/actor-following-property-required.ts"; + +const ruleName = RULE_IDS.actorFollowingPropertyRequired; const config = { rule, ruleName }; diff --git a/packages/lint/src/tests/actor-id-mismatch.test.ts b/packages/lint/src/tests/actor-id-mismatch.test.ts index 133626428..4b7a4ee93 100644 --- a/packages/lint/src/tests/actor-id-mismatch.test.ts +++ b/packages/lint/src/tests/actor-id-mismatch.test.ts @@ -1,11 +1,10 @@ import { test } from "node:test"; -import { properties } from "../lib/const.ts"; +import { properties, RULE_IDS } from "../lib/const.ts"; import { actorPropertyMismatch } from "../lib/messages.ts"; -import { testDenoLint } from "../lib/test.ts"; -import { - ACTOR_ID_MISMATCH as ruleName, - default as rule, -} from "../rules/actor-id-mismatch.ts"; +import lintTest from "../lib/test.ts"; +import * as rule from "../rules/actor-id-mismatch.ts"; + +const ruleName = RULE_IDS.actorIdMismatch; const expectedError = actorPropertyMismatch({ path: properties.id.path.join("."), @@ -15,8 +14,9 @@ const expectedError = actorPropertyMismatch({ requiresIdentifier: properties.id.requiresIdentifier, }); -test(`${ruleName}: ✅ Good - \`setActorDispatcher\` called on non-Federation object`, () => { - testDenoLint({ +test( + `${ruleName}: ✅ Good - \`setActorDispatcher\` called on non-Federation object`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { return new Person({ @@ -32,11 +32,12 @@ test(`${ruleName}: ✅ Good - \`setActorDispatcher\` called on non-Federation ob setActorDispatcher: () => {} }; `, - }); -}); + }), +); -test(`${ruleName}: ✅ Good - id uses ctx.getActorUri(identifier)`, () => { - testDenoLint({ +test( + `${ruleName}: ✅ Good - id uses ctx.getActorUri(identifier)`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { return new Person({ @@ -47,11 +48,12 @@ test(`${ruleName}: ✅ Good - id uses ctx.getActorUri(identifier)`, () => { `, rule, ruleName, - }); -}); + }), +); -test(`${ruleName}: ✅ Good - BlockStatement with correct id`, () => { - testDenoLint({ +test( + `${ruleName}: ✅ Good - BlockStatement with correct id`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { const name = "John Doe"; @@ -63,11 +65,12 @@ test(`${ruleName}: ✅ Good - BlockStatement with correct id`, () => { `, rule, ruleName, - }); -}); + }), +); -test(`${ruleName}: ❌ Bad - id uses hardcoded string instead of ctx.getActorUri()`, () => { - testDenoLint({ +test( + `${ruleName}: ❌ Bad - id uses hardcoded string instead of ctx.getActorUri()`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { return new Person({ @@ -79,11 +82,12 @@ test(`${ruleName}: ❌ Bad - id uses hardcoded string instead of ctx.getActorUri rule, ruleName, expectedError: expectedError, - }); -}); + }), +); -test(`${ruleName}: ❌ Bad - id uses wrong method (getInboxUri instead of getActorUri)`, () => { - testDenoLint({ +test( + `${ruleName}: ❌ Bad - id uses wrong method (getInboxUri instead of getActorUri)`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { return new Person({ @@ -95,11 +99,12 @@ test(`${ruleName}: ❌ Bad - id uses wrong method (getInboxUri instead of getAct rule, ruleName, expectedError: expectedError, - }); -}); + }), +); -test(`${ruleName}: ❌ Bad - id uses wrong identifier parameter`, () => { - testDenoLint({ +test( + `${ruleName}: ❌ Bad - id uses wrong identifier parameter`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { return new Person({ @@ -111,11 +116,12 @@ test(`${ruleName}: ❌ Bad - id uses wrong identifier parameter`, () => { rule, ruleName, expectedError: expectedError, - }); -}); + }), +); -test(`${ruleName} Edge: ✅ multiple return statements - all correct`, () => { - testDenoLint({ +test( + `${ruleName} Edge: ✅ multiple return statements - all correct`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { if (identifier === "admin") { @@ -126,11 +132,12 @@ test(`${ruleName} Edge: ✅ multiple return statements - all correct`, () => { `, rule, ruleName, - }); -}); + }), +); -test(`${ruleName} Edge: ⚠️ multiple returns - known limitation`, () => { - testDenoLint({ +test( + `${ruleName} Edge: ⚠️ multiple returns - known limitation`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { if (identifier === "admin") { @@ -143,11 +150,12 @@ test(`${ruleName} Edge: ⚠️ multiple returns - known limitation`, () => { ruleName, // Known limitation: Once ANY return has correct id, the rule passes. // The first return with wrong id is not caught. - }); -}); + }), +); -test(`${ruleName} Edge: ✅ spread operator with correct id after spread`, () => { - testDenoLint({ +test( + `${ruleName} Edge: ✅ spread operator with correct id after spread`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { const base = { name: "User" }; @@ -156,11 +164,12 @@ test(`${ruleName} Edge: ✅ spread operator with correct id after spread`, () => `, rule, ruleName, - }); -}); + }), +); -test(`${ruleName} Edge: ❌ spread operator with wrong id after spread`, () => { - testDenoLint({ +test( + `${ruleName} Edge: ❌ spread operator with wrong id after spread`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { const base = { name: "User" }; @@ -170,11 +179,12 @@ test(`${ruleName} Edge: ❌ spread operator with wrong id after spread`, () => { rule, ruleName, expectedError: expectedError, - }); -}); + }), +); -test(`${ruleName} Edge: ✅ arrow function direct return with correct id`, () => { - testDenoLint({ +test( + `${ruleName} Edge: ✅ arrow function direct return with correct id`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { return new Person({ @@ -185,11 +195,12 @@ test(`${ruleName} Edge: ✅ arrow function direct return with correct id`, () => `, rule, ruleName, - }); -}); + }), +); -test(`${ruleName} Edge: ❌ arrow function direct return with wrong id`, () => { - testDenoLint({ +test( + `${ruleName} Edge: ❌ arrow function direct return with wrong id`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { return new Person({ @@ -201,5 +212,5 @@ test(`${ruleName} Edge: ❌ arrow function direct return with wrong id`, () => { rule, ruleName, expectedError: expectedError, - }); -}); + }), +); diff --git a/packages/lint/src/tests/actor-id-required.test.ts b/packages/lint/src/tests/actor-id-required.test.ts index 311b5fe0c..ac7d94dd3 100644 --- a/packages/lint/src/tests/actor-id-required.test.ts +++ b/packages/lint/src/tests/actor-id-required.test.ts @@ -1,14 +1,14 @@ import { test } from "node:test"; -import { properties } from "../lib/const.ts"; +import { properties, RULE_IDS } from "../lib/const.ts"; import { actorPropertyRequired } from "../lib/messages.ts"; -import { testDenoLint } from "../lib/test.ts"; -import { - ACTOR_ID_REQUIRED as ruleName, - default as rule, -} from "../rules/actor-id-required.ts"; +import lintTest from "../lib/test.ts"; +import * as rule from "../rules/actor-id-required.ts"; -test(`${ruleName}: ✅ Good - \`setActorDispatcher\` called on non-Federation object`, () => { - testDenoLint({ +const ruleName = RULE_IDS.actorIdRequired; + +test( + `${ruleName}: ✅ Good - \`setActorDispatcher\` called on non-Federation object`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { return new Person({ @@ -23,11 +23,12 @@ test(`${ruleName}: ✅ Good - \`setActorDispatcher\` called on non-Federation ob setActorDispatcher: () => {} }; `, - }); -}); + }), +); -test(`${ruleName}: ✅ Good - with \`id\` property (any value)`, () => { - testDenoLint({ +test( + `${ruleName}: ✅ Good - with \`id\` property (any value)`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { return new Person({ @@ -38,11 +39,12 @@ test(`${ruleName}: ✅ Good - with \`id\` property (any value)`, () => { `, rule, ruleName, - }); -}); + }), +); -test(`${ruleName}: ✅ Good - with \`id\` property using ctx.getActorUri()`, () => { - testDenoLint({ +test( + `${ruleName}: ✅ Good - with \`id\` property using ctx.getActorUri()`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { return new Person({ @@ -53,11 +55,12 @@ test(`${ruleName}: ✅ Good - with \`id\` property using ctx.getActorUri()`, () `, rule, ruleName, - }); -}); + }), +); -test(`${ruleName}: ✅ Good - BlockStatement with \`id\``, () => { - testDenoLint({ +test( + `${ruleName}: ✅ Good - BlockStatement with \`id\``, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { const name = "John Doe"; @@ -69,11 +72,12 @@ test(`${ruleName}: ✅ Good - BlockStatement with \`id\``, () => { `, rule, ruleName, - }); -}); + }), +); -test(`${ruleName}: ❌ Bad - without \`id\` property`, () => { - testDenoLint({ +test( + `${ruleName}: ❌ Bad - without \`id\` property`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { return new Person({ @@ -84,11 +88,12 @@ test(`${ruleName}: ❌ Bad - without \`id\` property`, () => { rule, ruleName, expectedError: actorPropertyRequired(properties.id), - }); -}); + }), +); -test(`${ruleName}: ❌ Bad - returning empty object`, () => { - testDenoLint({ +test( + `${ruleName}: ❌ Bad - returning empty object`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { return new Person({}); @@ -97,11 +102,12 @@ test(`${ruleName}: ❌ Bad - returning empty object`, () => { rule, ruleName, expectedError: actorPropertyRequired(properties.id), - }); -}); + }), +); -test(`${ruleName}: ✅ Good - multiple properties including \`id\``, () => { - testDenoLint({ +test( + `${ruleName}: ✅ Good - multiple properties including \`id\``, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { return new Person({ @@ -114,11 +120,12 @@ test(`${ruleName}: ✅ Good - multiple properties including \`id\``, () => { `, rule, ruleName, - }); -}); + }), +); -test(`${ruleName}: ❌ Bad - variable assignment without \`id\``, () => { - testDenoLint({ +test( + `${ruleName}: ❌ Bad - variable assignment without \`id\``, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { const actor = new Person({ @@ -130,140 +137,143 @@ test(`${ruleName}: ❌ Bad - variable assignment without \`id\``, () => { rule, ruleName, expectedError: actorPropertyRequired(properties.id), - }); -}); - -// ============================================================================= -// Edge Cases -// ============================================================================= - -const withId = (extra = "") => - `new Person({ id: ctx.getActorUri(identifier), name: "User"${extra} })`; -const withoutId = () => `new Person({ name: "User" })`; + }), +); +const withId = 'new Person({ id: ctx.getActorUri(identifier), name: "User" })'; +const withoutId = 'new Person({ name: "User" })'; -test(`${ruleName}: ✅ Edge - multiple return statements - all have id`, () => { - testDenoLint({ +test( + `${ruleName}: ✅ Edge - multiple return statements - all have id`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { if (identifier === "admin") { - return ${withId()}; + return ${withId}; } return identifier === "admin" - ? ${withId()} - : ${withId()}; + ? ${withId} + : ${withId}; }); `, rule, ruleName, - }); -}); + }), +); -test(`${ruleName}: ✅ Edge - multiple return statements - first missing id (known limitation)`, () => { - testDenoLint({ +test( + `${ruleName}: ✅ Edge - multiple return statements - first missing id (known limitation)`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { if (identifier === "admin") { - return ${withoutId()}; + return ${withoutId}; } - return ${withId()}; + return ${withId}; }); `, rule, ruleName, - }); -}); + }), +); -test(`${ruleName}: ❌ Edge - multiple return statements - second missing id`, () => { - testDenoLint({ +test( + `${ruleName}: ❌ Edge - multiple return statements - second missing id`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { if (identifier === "admin") { - return ${withId()}; + return ${withId}; } - return ${withoutId()}; + return ${withoutId}; }); `, rule, ruleName, expectedError: actorPropertyRequired(properties.id), - }); -}); + }), +); -test(`${ruleName}: ✅ Edge - if/else with else block (known limitation: else not checked)`, () => { - testDenoLint({ +test( + `${ruleName}: ✅ Edge - if/else with else block (known limitation: else not checked)`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { if (identifier) { - return ${withId()}; + return ${withId}; } else { - return ${withoutId()}; + return ${withoutId}; } - return ${withId()}; + return ${withId}; }); `, rule, ruleName, - }); -}); + }), +); -test(`${ruleName}: ✅ Edge - nested if with id`, () => { - testDenoLint({ +test( + `${ruleName}: ✅ Edge - nested if with id`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { if (identifier) { if (identifier === "admin") { - return ${withId()}; + return ${withId}; } - return ${withId()}; + return ${withId}; } - return ${withId()}; + return ${withId}; }); `, rule, ruleName, - }); -}); + }), +); -test(`${ruleName}: ✅ Edge - ternary operator with id in both branches`, () => { - testDenoLint({ +test( + `${ruleName}: ✅ Edge - ternary operator with id in both branches`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return identifier ? ${withId()} : ${withId()}; + return identifier ? ${withId} : ${withId}; }); `, rule, ruleName, - }); -}); + }), +); -test(`${ruleName}: ❌ Edge - ternary operator without id in consequent`, () => { - testDenoLint({ +test( + `${ruleName}: ❌ Edge - ternary operator without id in consequent`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return identifier ? ${withoutId()} : ${withId()}; + return identifier ? ${withoutId} : ${withId}; }); `, rule, ruleName, expectedError: actorPropertyRequired(properties.id), - }); -}); + }), +); -test(`${ruleName}: ❌ Edge - ternary operator without id in alternate`, () => { - testDenoLint({ +test( + `${ruleName}: ❌ Edge - ternary operator without id in alternate`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return identifier ? ${withId()} : ${withoutId()}; + return identifier ? ${withId} : ${withoutId}; }); `, rule, ruleName, expectedError: actorPropertyRequired(properties.id), - }); -}); + }), +); -test(`${ruleName}: ✅ Edge - spread operator with id property after spread`, () => { - testDenoLint({ +test( + `${ruleName}: ✅ Edge - spread operator with id property after spread`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { const base = { name: "User" }; @@ -272,11 +282,12 @@ test(`${ruleName}: ✅ Edge - spread operator with id property after spread`, () `, rule, ruleName, - }); -}); + }), +); -test(`${ruleName}: ❌ Edge - spread operator with id in spread source (known limitation)`, () => { - testDenoLint({ +test( + `${ruleName}: ❌ Edge - spread operator with id in spread source (known limitation)`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { const base = { id: ctx.getActorUri(identifier), name: "User" }; @@ -286,28 +297,30 @@ test(`${ruleName}: ❌ Edge - spread operator with id in spread source (known li rule, ruleName, expectedError: actorPropertyRequired(properties.id), - }); -}); + }), +); -test(`${ruleName}: ❌ Edge - variable assignment then return (known limitation)`, () => { - testDenoLint({ +test( + `${ruleName}: ❌ Edge - variable assignment then return (known limitation)`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - const actor = ${withId()}; + const actor = ${withId}; return actor; }); `, rule, ruleName, expectedError: actorPropertyRequired(properties.id), - }); -}); + }), +); -test(`${ruleName}: ❌ Edge - property assignment after construction (known limitation)`, () => { - testDenoLint({ +test( + `${ruleName}: ❌ Edge - property assignment after construction (known limitation)`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - const actor = ${withoutId()}; + const actor = ${withoutId}; actor.id = ctx.getActorUri(identifier); return actor; }); @@ -315,30 +328,32 @@ test(`${ruleName}: ❌ Edge - property assignment after construction (known limi rule, ruleName, expectedError: actorPropertyRequired(properties.id), - }); -}); + }), +); -test(`${ruleName}: ✅ Edge - arrow function direct return NewExpression`, () => { - testDenoLint({ +test( + `${ruleName}: ✅ Edge - arrow function direct return NewExpression`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => - ${withId()} + ${withId} ); `, rule, ruleName, - }); -}); + }), +); -test(`${ruleName}: ✅ Edge - return null (no actor)`, () => { - testDenoLint({ +test( + `${ruleName}: ✅ Edge - return null (no actor)`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { if (!identifier) return null; - return ${withId()}; + return ${withId}; }); `, rule, ruleName, - }); -}); + }), +); diff --git a/packages/lint/src/tests/actor-inbox-property-mismatch.test.ts b/packages/lint/src/tests/actor-inbox-property-mismatch.test.ts index 9ff6c5894..977df1981 100644 --- a/packages/lint/src/tests/actor-inbox-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-inbox-property-mismatch.test.ts @@ -1,12 +1,12 @@ import { test } from "node:test"; +import { RULE_IDS } from "../lib/const.ts"; import { createMismatchEdgeCaseTests, createMismatchRuleTests, } from "../lib/test-templates.ts"; -import { - ACTOR_INBOX_PROPERTY_MISMATCH as ruleName, - default as rule, -} from "../rules/actor-inbox-property-mismatch.ts"; +import * as rule from "../rules/actor-inbox-property-mismatch.ts"; + +const ruleName = RULE_IDS.actorInboxPropertyMismatch; const config = { rule, ruleName }; diff --git a/packages/lint/src/tests/actor-inbox-property-required.test.ts b/packages/lint/src/tests/actor-inbox-property-required.test.ts index f42cf7e2b..5e8f2683f 100644 --- a/packages/lint/src/tests/actor-inbox-property-required.test.ts +++ b/packages/lint/src/tests/actor-inbox-property-required.test.ts @@ -1,12 +1,12 @@ import { test } from "node:test"; +import { RULE_IDS } from "../lib/const.ts"; import { createRequiredEdgeCaseTests, createRequiredListenerRuleTests, } from "../lib/test-templates.ts"; -import { - ACTOR_INBOX_PROPERTY_REQUIRED as ruleName, - default as rule, -} from "../rules/actor-inbox-property-required.ts"; +import * as rule from "../rules/actor-inbox-property-required.ts"; + +const ruleName = RULE_IDS.actorInboxPropertyRequired; const config = { rule, ruleName }; diff --git a/packages/lint/src/tests/actor-liked-property-mismatch.test.ts b/packages/lint/src/tests/actor-liked-property-mismatch.test.ts index 62a3a5007..6d6a872b2 100644 --- a/packages/lint/src/tests/actor-liked-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-liked-property-mismatch.test.ts @@ -1,12 +1,12 @@ import { test } from "node:test"; +import { RULE_IDS } from "../lib/const.ts"; import { createMismatchEdgeCaseTests, createMismatchRuleTests, } from "../lib/test-templates.ts"; -import { - ACTOR_LIKED_PROPERTY_MISMATCH as ruleName, - default as rule, -} from "../rules/actor-liked-property-mismatch.ts"; +import * as rule from "../rules/actor-liked-property-mismatch.ts"; + +const ruleName = RULE_IDS.actorLikedPropertyMismatch; const config = { rule, ruleName }; diff --git a/packages/lint/src/tests/actor-liked-property-required.test.ts b/packages/lint/src/tests/actor-liked-property-required.test.ts index 22fad62de..825cee229 100644 --- a/packages/lint/src/tests/actor-liked-property-required.test.ts +++ b/packages/lint/src/tests/actor-liked-property-required.test.ts @@ -1,12 +1,12 @@ import { test } from "node:test"; +import { RULE_IDS } from "../lib/const.ts"; import { createRequiredDispatcherRuleTests, createRequiredEdgeCaseTests, } from "../lib/test-templates.ts"; -import { - ACTOR_LIKED_PROPERTY_REQUIRED as ruleName, - default as rule, -} from "../rules/actor-liked-property-required.ts"; +import * as rule from "../rules/actor-liked-property-required.ts"; + +const ruleName = RULE_IDS.actorLikedPropertyRequired; const config = { rule, ruleName }; diff --git a/packages/lint/src/tests/actor-outbox-property-mismatch.test.ts b/packages/lint/src/tests/actor-outbox-property-mismatch.test.ts index d5cc49b4c..54e9d317a 100644 --- a/packages/lint/src/tests/actor-outbox-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-outbox-property-mismatch.test.ts @@ -1,12 +1,12 @@ import { test } from "node:test"; +import { RULE_IDS } from "../lib/const.ts"; import { createMismatchEdgeCaseTests, createMismatchRuleTests, } from "../lib/test-templates.ts"; -import { - ACTOR_OUTBOX_PROPERTY_MISMATCH as ruleName, - default as rule, -} from "../rules/actor-outbox-property-mismatch.ts"; +import * as rule from "../rules/actor-outbox-property-mismatch.ts"; + +const ruleName = RULE_IDS.actorOutboxPropertyMismatch; const config = { rule, ruleName }; diff --git a/packages/lint/src/tests/actor-outbox-property-required.test.ts b/packages/lint/src/tests/actor-outbox-property-required.test.ts index c55038292..1e44d9a80 100644 --- a/packages/lint/src/tests/actor-outbox-property-required.test.ts +++ b/packages/lint/src/tests/actor-outbox-property-required.test.ts @@ -1,12 +1,12 @@ import { test } from "node:test"; +import { RULE_IDS } from "../lib/const.ts"; import { createRequiredDispatcherRuleTests, createRequiredEdgeCaseTests, } from "../lib/test-templates.ts"; -import { - ACTOR_OUTBOX_PROPERTY_REQUIRED as ruleName, - default as rule, -} from "../rules/actor-outbox-property-required.ts"; +import * as rule from "../rules/actor-outbox-property-required.ts"; + +const ruleName = RULE_IDS.actorOutboxPropertyRequired; const config = { rule, ruleName }; diff --git a/packages/lint/src/tests/actor-public-key-required.test.ts b/packages/lint/src/tests/actor-public-key-required.test.ts index ce5dc7eba..fd44c6c62 100644 --- a/packages/lint/src/tests/actor-public-key-required.test.ts +++ b/packages/lint/src/tests/actor-public-key-required.test.ts @@ -1,14 +1,14 @@ import { test } from "node:test"; -import { properties } from "../lib/const.ts"; +import { properties, RULE_IDS } from "../lib/const.ts"; import { actorPropertyRequired } from "../lib/messages.ts"; -import { testDenoLint } from "../lib/test.ts"; -import { - ACTOR_PUBLIC_KEY_REQUIRED as ruleName, - default as rule, -} from "../rules/actor-public-key-required.ts"; +import lintTest from "../lib/test.ts"; +import * as rule from "../rules/actor-public-key-required.ts"; -test(`${ruleName}: ✅ Good - \`setActorDispatcher\` called on non-Federation object`, () => { - testDenoLint({ +const ruleName = RULE_IDS.actorPublicKeyRequired; + +test( + `${ruleName}: ✅ Good - \`setActorDispatcher\` called on non-Federation object`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { return new Person({ @@ -23,11 +23,12 @@ test(`${ruleName}: ✅ Good - \`setActorDispatcher\` called on non-Federation ob setActorDispatcher: () => {} }; `, - }); -}); + }), +); -test(`${ruleName}: ✅ Good - key pairs dispatcher NOT configured, property missing (no error)`, () => { - testDenoLint({ +test( + `${ruleName}: ✅ Good - key pairs dispatcher NOT configured, property missing (no error)`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { return new Person({ @@ -39,11 +40,12 @@ test(`${ruleName}: ✅ Good - key pairs dispatcher NOT configured, property miss `, rule, ruleName, - }); -}); + }), +); -test(`${ruleName}: ✅ Good - key pairs dispatcher configured BEFORE setActorDispatcher, property present`, () => { - testDenoLint({ +test( + `${ruleName}: ✅ Good - key pairs dispatcher configured BEFORE setActorDispatcher, property present`, + lintTest({ code: ` federation .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { @@ -57,11 +59,12 @@ test(`${ruleName}: ✅ Good - key pairs dispatcher configured BEFORE setActorDis `, rule, ruleName, - }); -}); + }), +); -test(`${ruleName}: ✅ Good - key pairs dispatcher configured BEFORE setActorDispatcher (separate calls), property present`, () => { - testDenoLint({ +test( + `${ruleName}: ✅ Good - key pairs dispatcher configured BEFORE setActorDispatcher (separate calls), property present`, + lintTest({ code: ` federation.setKeyPairsDispatcher(async (ctx, identifier) => []); @@ -75,11 +78,12 @@ test(`${ruleName}: ✅ Good - key pairs dispatcher configured BEFORE setActorDis `, rule, ruleName, - }); -}); + }), +); -test(`${ruleName}: ✅ Good - key pairs dispatcher configured AFTER setActorDispatcher, property present`, () => { - testDenoLint({ +test( + `${ruleName}: ✅ Good - key pairs dispatcher configured AFTER setActorDispatcher, property present`, + lintTest({ code: ` federation .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { @@ -93,11 +97,12 @@ test(`${ruleName}: ✅ Good - key pairs dispatcher configured AFTER setActorDisp `, rule, ruleName, - }); -}); + }), +); -test(`${ruleName}: ✅ Good - key pairs dispatcher configured AFTER setActorDispatcher (separate calls), property present`, () => { - testDenoLint({ +test( + `${ruleName}: ✅ Good - key pairs dispatcher configured AFTER setActorDispatcher (separate calls), property present`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { const keyPairs = await ctx.getActorKeyPairs(identifier); @@ -111,11 +116,12 @@ test(`${ruleName}: ✅ Good - key pairs dispatcher configured AFTER setActorDisp `, rule, ruleName, - }); -}); + }), +); -test(`${ruleName}: ❌ Bad - key pairs dispatcher configured BEFORE, property missing`, () => { - testDenoLint({ +test( + `${ruleName}: ❌ Bad - key pairs dispatcher configured BEFORE, property missing`, + lintTest({ code: ` federation .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { @@ -129,11 +135,12 @@ test(`${ruleName}: ❌ Bad - key pairs dispatcher configured BEFORE, property mi rule, ruleName, expectedError: actorPropertyRequired(properties.publicKey), - }); -}); + }), +); -test(`${ruleName}: ❌ Bad - key pairs dispatcher configured BEFORE (separate calls), property missing`, () => { - testDenoLint({ +test( + `${ruleName}: ❌ Bad - key pairs dispatcher configured BEFORE (separate calls), property missing`, + lintTest({ code: ` federation.setKeyPairsDispatcher(async (ctx, identifier) => []); @@ -147,11 +154,12 @@ test(`${ruleName}: ❌ Bad - key pairs dispatcher configured BEFORE (separate ca rule, ruleName, expectedError: actorPropertyRequired(properties.publicKey), - }); -}); + }), +); -test(`${ruleName}: ❌ Bad - key pairs dispatcher configured AFTER, property missing`, () => { - testDenoLint({ +test( + `${ruleName}: ❌ Bad - key pairs dispatcher configured AFTER, property missing`, + lintTest({ code: ` federation .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { @@ -164,11 +172,12 @@ test(`${ruleName}: ❌ Bad - key pairs dispatcher configured AFTER, property mis rule, ruleName, expectedError: actorPropertyRequired(properties.publicKey), - }); -}); + }), +); -test(`${ruleName}: ❌ Bad - key pairs dispatcher configured AFTER (separate calls), property missing`, () => { - testDenoLint({ +test( + `${ruleName}: ❌ Bad - key pairs dispatcher configured AFTER (separate calls), property missing`, + lintTest({ code: ` federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { return new Person({ @@ -181,5 +190,5 @@ test(`${ruleName}: ❌ Bad - key pairs dispatcher configured AFTER (separate cal rule, ruleName, expectedError: actorPropertyRequired(properties.publicKey), - }); -}); + }), +); diff --git a/packages/lint/src/tests/actor-shared-inbox-property-mismatch.test.ts b/packages/lint/src/tests/actor-shared-inbox-property-mismatch.test.ts index 058e6425d..dc4d4c484 100644 --- a/packages/lint/src/tests/actor-shared-inbox-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-shared-inbox-property-mismatch.test.ts @@ -1,12 +1,12 @@ import { test } from "node:test"; +import { RULE_IDS } from "../lib/const.ts"; import { createMismatchEdgeCaseTests, createMismatchRuleTests, } from "../lib/test-templates.ts"; -import { - ACTOR_SHARED_INBOX_PROPERTY_MISMATCH as ruleName, - default as rule, -} from "../rules/actor-shared-inbox-property-mismatch.ts"; +import * as rule from "../rules/actor-shared-inbox-property-mismatch.ts"; + +const ruleName = RULE_IDS.actorSharedInboxPropertyMismatch; const config = { rule, ruleName }; diff --git a/packages/lint/src/tests/actor-shared-inbox-property-required.test.ts b/packages/lint/src/tests/actor-shared-inbox-property-required.test.ts index 254b798d3..3ad58e56f 100644 --- a/packages/lint/src/tests/actor-shared-inbox-property-required.test.ts +++ b/packages/lint/src/tests/actor-shared-inbox-property-required.test.ts @@ -1,12 +1,12 @@ import { test } from "node:test"; +import { RULE_IDS } from "../lib/const.ts"; import { createRequiredEdgeCaseTests, createRequiredListenerRuleTests, } from "../lib/test-templates.ts"; -import { - ACTOR_SHARED_INBOX_PROPERTY_REQUIRED as ruleName, - default as rule, -} from "../rules/actor-shared-inbox-property-required.ts"; +import * as rule from "../rules/actor-shared-inbox-property-required.ts"; + +const ruleName = RULE_IDS.actorSharedInboxPropertyRequired; const config = { rule, ruleName }; diff --git a/packages/lint/src/tests/collection-filtering-not-implemented.test.ts b/packages/lint/src/tests/collection-filtering-not-implemented.test.ts index 341b9131e..7e57c10a6 100644 --- a/packages/lint/src/tests/collection-filtering-not-implemented.test.ts +++ b/packages/lint/src/tests/collection-filtering-not-implemented.test.ts @@ -1,12 +1,10 @@ import { test } from "node:test"; -import { properties } from "../lib/const.ts"; +import { properties, RULE_IDS } from "../lib/const.ts"; import { COLLECTION_FILTERING_NOT_IMPLEMENTED_ERROR as expectedError } from "../lib/messages.ts"; -import { testDenoLint } from "../lib/test.ts"; -import { - COLLECTION_FILTERING_NOT_IMPLEMENTED as ruleName, - default as rule, -} from "../rules/collection-filtering-not-implemented.ts"; +import lintTest from "../lib/test.ts"; +import * as rule from "../rules/collection-filtering-not-implemented.ts"; +const ruleName = RULE_IDS.collectionFilteringNotImplemented; const filterless = ["ctx", "identifier", "cursor"] as const; // Helper to create test code for setFollowersDispatcher @@ -37,117 +35,108 @@ const createFollowersDispatcherCode = ( test( `${ruleName}: ✅ Good - async arrow function with filter parameter`, - () => - testDenoLint({ - code: createFollowersDispatcherCode(), - rule, - ruleName, - }), + lintTest({ + code: createFollowersDispatcherCode(), + rule, + ruleName, + }), ); test( `${ruleName}: ✅ Good - async function expression with filter`, - () => - testDenoLint({ - code: createFollowersDispatcherCode({ arrow: false }), - rule, - ruleName, - }), + lintTest({ + code: createFollowersDispatcherCode({ arrow: false }), + rule, + ruleName, + }), ); test( `${ruleName}: ✅ Good - sync arrow function with filter`, - () => - testDenoLint({ - code: createFollowersDispatcherCode({ async: false }), - rule, - ruleName, - }), + lintTest({ + code: createFollowersDispatcherCode({ async: false }), + rule, + ruleName, + }), ); test( `${ruleName}: ✅ Good - sync function expression with filter`, - () => - testDenoLint({ - code: createFollowersDispatcherCode({ async: false, arrow: false }), - rule, - ruleName, - }), + lintTest({ + code: createFollowersDispatcherCode({ async: false, arrow: false }), + rule, + ruleName, + }), ); test( `${ruleName}: ❌ Bad - async arrow function without filter parameter`, - () => - testDenoLint({ - code: createFollowersDispatcherCode({ params: filterless }), - rule, - ruleName, - expectedError, - }), + lintTest({ + code: createFollowersDispatcherCode({ params: filterless }), + rule, + ruleName, + expectedError, + }), ); test( `${ruleName}: ❌ Bad - async function expression without filter`, - () => - testDenoLint({ - code: createFollowersDispatcherCode({ params: filterless, arrow: false }), - rule, - ruleName, - expectedError, - }), + lintTest({ + code: createFollowersDispatcherCode({ params: filterless, arrow: false }), + rule, + ruleName, + expectedError, + }), ); test( `${ruleName}: ❌ Bad - sync arrow function expression without filter`, - () => - testDenoLint({ - code: createFollowersDispatcherCode({ params: filterless, async: false }), - rule, - ruleName, - expectedError, - }), + lintTest({ + code: createFollowersDispatcherCode({ params: filterless, async: false }), + rule, + ruleName, + expectedError, + }), ); test( `${ruleName}: ❌ Bad - sync function expression without filter`, - () => - testDenoLint({ - code: createFollowersDispatcherCode({ - params: filterless, - async: false, - arrow: false, - }), - rule, - ruleName, - expectedError, + lintTest({ + code: createFollowersDispatcherCode({ + params: filterless, + async: false, + arrow: false, }), + rule, + ruleName, + expectedError, + }), ); test( `${ruleName}: ✅ Good - 4th parameter but unnamed filter`, - () => - testDenoLint({ - code: createFollowersDispatcherCode({ - params: ["ctx", "identifier", "cursor", "baseUri"], - }), - rule, - ruleName, + lintTest({ + code: createFollowersDispatcherCode({ + params: ["ctx", "identifier", "cursor", "baseUri"], }), + rule, + ruleName, + }), ); test( `${ruleName}: ❌ Bad - only two parameters (missing cursor and filter)`, - () => - testDenoLint({ - code: createFollowersDispatcherCode({ params: ["ctx", "identifier"] }), - rule, - ruleName, - expectedError, - }), + lintTest({ + code: createFollowersDispatcherCode({ params: ["ctx", "identifier"] }), + rule, + ruleName, + expectedError, + }), ); -test(`${ruleName}: ✅ Good - non-federation object is not checked`, () => - testDenoLint({ +test( + `${ruleName}: ✅ Good - non-federation object is not checked`, + lintTest({ code: createFollowersDispatcherCode({ params: filterless }), rule, ruleName, @@ -156,7 +145,8 @@ test(`${ruleName}: ✅ Good - non-federation object is not checked`, () => setFollowersDispatcher: () => {} }; `, - })); + }), +); // Test that other collection dispatchers are NOT checked const otherDispatchers = [ @@ -167,23 +157,18 @@ const otherDispatchers = [ "featuredTags", ] as const satisfies (keyof typeof properties)[]; +const paramsString = filterless.join(", "); + test( `${ruleName}: ✅ Good - other collection dispatchers without filter are NOT checked`, - () => - otherDispatchers.forEach((name) => { - const paramsString = filterless.join(", "); - testDenoLint({ - code: ` - federation.${properties[name].setter}( - "/users/{identifier}/${name}", - async (${paramsString}) => { - return { items: [] }; - } - ); - `, - rule, - ruleName, - // No expectedError - these should pass without filter - }); - }), + lintTest({ + code: otherDispatchers.map((name) => + `federation.${properties[name].setter}( + "/users/{identifier}/${name}", + async (${paramsString}) => { items: [] }, + );` + ).join("\n"), + rule, + ruleName, + }), ); diff --git a/packages/lint/src/tests/eslint/actor-assertion-method-required.test.ts b/packages/lint/src/tests/eslint/actor-assertion-method-required.test.ts deleted file mode 100644 index 2fc65da0c..000000000 --- a/packages/lint/src/tests/eslint/actor-assertion-method-required.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * ESLint tests for actor-assertion-method-required rule. - */ -import { test } from "node:test"; -import { rules, RULE_IDS } from "../../eslint.ts"; -import { runESLintTests } from "../../lib/test-eslint.ts"; -import { - createRequiredInvalidCases, - createRequiredValidCases, -} from "../../lib/test-templates-eslint.ts"; - -const ruleName = RULE_IDS.actorAssertionMethodRequired; -const rule = rules[ruleName]; - -test(`ESLint: ${ruleName}`, () => { - runESLintTests(ruleName, rule, { - valid: createRequiredValidCases("assertionMethod"), - invalid: createRequiredInvalidCases("assertionMethod", "required"), - }); -}); diff --git a/packages/lint/src/tests/eslint/actor-featured-property-mismatch.test.ts b/packages/lint/src/tests/eslint/actor-featured-property-mismatch.test.ts deleted file mode 100644 index d1b80220f..000000000 --- a/packages/lint/src/tests/eslint/actor-featured-property-mismatch.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * ESLint tests for actor-featured-property-mismatch rule. - */ -import { test } from "node:test"; -import { rules, RULE_IDS } from "../../eslint.ts"; -import { runESLintTests } from "../../lib/test-eslint.ts"; -import { - createMismatchInvalidCases, - createMismatchValidCases, -} from "../../lib/test-templates-eslint.ts"; - -const ruleName = RULE_IDS.actorFeaturedPropertyMismatch; -const rule = rules[ruleName]; - -test(`ESLint: ${ruleName}`, () => { - runESLintTests(ruleName, rule, { - valid: createMismatchValidCases("featured"), - invalid: createMismatchInvalidCases("featured", "mismatch"), - }); -}); diff --git a/packages/lint/src/tests/eslint/actor-featured-property-required.test.ts b/packages/lint/src/tests/eslint/actor-featured-property-required.test.ts deleted file mode 100644 index cd5df000f..000000000 --- a/packages/lint/src/tests/eslint/actor-featured-property-required.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * ESLint tests for actor-featured-property-required rule. - */ -import { test } from "node:test"; -import { rules, RULE_IDS } from "../../eslint.ts"; -import { runESLintTests } from "../../lib/test-eslint.ts"; -import { - createRequiredInvalidCases, - createRequiredValidCases, -} from "../../lib/test-templates-eslint.ts"; - -const ruleName = RULE_IDS.actorFeaturedPropertyRequired; -const rule = rules[ruleName]; - -test(`ESLint: ${ruleName}`, () => { - runESLintTests(ruleName, rule, { - valid: createRequiredValidCases("featured"), - invalid: createRequiredInvalidCases("featured", "required"), - }); -}); diff --git a/packages/lint/src/tests/eslint/actor-featured-tags-property-mismatch.test.ts b/packages/lint/src/tests/eslint/actor-featured-tags-property-mismatch.test.ts deleted file mode 100644 index d1779b224..000000000 --- a/packages/lint/src/tests/eslint/actor-featured-tags-property-mismatch.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * ESLint tests for actor-featured-tags-property-mismatch rule. - */ -import { test } from "node:test"; -import { rules, RULE_IDS } from "../../eslint.ts"; -import { runESLintTests } from "../../lib/test-eslint.ts"; -import { - createMismatchInvalidCases, - createMismatchValidCases, -} from "../../lib/test-templates-eslint.ts"; - -const ruleName = RULE_IDS.actorFeaturedTagsPropertyMismatch; -const rule = rules[ruleName]; - -test(`ESLint: ${ruleName}`, () => { - runESLintTests(ruleName, rule, { - valid: createMismatchValidCases("featuredTags"), - invalid: createMismatchInvalidCases("featuredTags", "mismatch"), - }); -}); diff --git a/packages/lint/src/tests/eslint/actor-featured-tags-property-required.test.ts b/packages/lint/src/tests/eslint/actor-featured-tags-property-required.test.ts deleted file mode 100644 index 3dc83b65a..000000000 --- a/packages/lint/src/tests/eslint/actor-featured-tags-property-required.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * ESLint tests for actor-featured-tags-property-required rule. - */ -import { test } from "node:test"; -import { rules, RULE_IDS } from "../../eslint.ts"; -import { runESLintTests } from "../../lib/test-eslint.ts"; -import { - createRequiredInvalidCases, - createRequiredValidCases, -} from "../../lib/test-templates-eslint.ts"; - -const ruleName = RULE_IDS.actorFeaturedTagsPropertyRequired; -const rule = rules[ruleName]; - -test(`ESLint: ${ruleName}`, () => { - runESLintTests(ruleName, rule, { - valid: createRequiredValidCases("featuredTags"), - invalid: createRequiredInvalidCases("featuredTags", "required"), - }); -}); diff --git a/packages/lint/src/tests/eslint/actor-followers-property-mismatch.test.ts b/packages/lint/src/tests/eslint/actor-followers-property-mismatch.test.ts deleted file mode 100644 index 336496b90..000000000 --- a/packages/lint/src/tests/eslint/actor-followers-property-mismatch.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * ESLint tests for actor-followers-property-mismatch rule. - */ -import { test } from "node:test"; -import { rules, RULE_IDS } from "../../eslint.ts"; -import { runESLintTests } from "../../lib/test-eslint.ts"; -import { - createMismatchInvalidCases, - createMismatchValidCases, -} from "../../lib/test-templates-eslint.ts"; - -const ruleName = RULE_IDS.actorFollowersPropertyMismatch; -const rule = rules[ruleName]; - -test(`ESLint: ${ruleName}`, () => { - runESLintTests(ruleName, rule, { - valid: createMismatchValidCases("followers"), - invalid: createMismatchInvalidCases("followers", "mismatch"), - }); -}); diff --git a/packages/lint/src/tests/eslint/actor-followers-property-required.test.ts b/packages/lint/src/tests/eslint/actor-followers-property-required.test.ts deleted file mode 100644 index f64bdfda6..000000000 --- a/packages/lint/src/tests/eslint/actor-followers-property-required.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * ESLint tests for actor-followers-property-required rule. - */ -import { test } from "node:test"; -import { rules, RULE_IDS } from "../../eslint.ts"; -import { runESLintTests } from "../../lib/test-eslint.ts"; -import { - createRequiredInvalidCases, - createRequiredValidCases, -} from "../../lib/test-templates-eslint.ts"; - -const ruleName = RULE_IDS.actorFollowersPropertyRequired; -const rule = rules[ruleName]; - -test(`ESLint: ${ruleName}`, () => { - runESLintTests(ruleName, rule, { - valid: createRequiredValidCases("followers"), - invalid: createRequiredInvalidCases("followers", "required"), - }); -}); diff --git a/packages/lint/src/tests/eslint/actor-following-property-mismatch.test.ts b/packages/lint/src/tests/eslint/actor-following-property-mismatch.test.ts deleted file mode 100644 index a958d3110..000000000 --- a/packages/lint/src/tests/eslint/actor-following-property-mismatch.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * ESLint tests for actor-following-property-mismatch rule. - */ -import { test } from "node:test"; -import { rules, RULE_IDS } from "../../eslint.ts"; -import { runESLintTests } from "../../lib/test-eslint.ts"; -import { - createMismatchInvalidCases, - createMismatchValidCases, -} from "../../lib/test-templates-eslint.ts"; - -const ruleName = RULE_IDS.actorFollowingPropertyMismatch; -const rule = rules[ruleName]; - -test(`ESLint: ${ruleName}`, () => { - runESLintTests(ruleName, rule, { - valid: createMismatchValidCases("following"), - invalid: createMismatchInvalidCases("following", "mismatch"), - }); -}); diff --git a/packages/lint/src/tests/eslint/actor-following-property-required.test.ts b/packages/lint/src/tests/eslint/actor-following-property-required.test.ts deleted file mode 100644 index 4227ddcb3..000000000 --- a/packages/lint/src/tests/eslint/actor-following-property-required.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * ESLint tests for actor-following-property-required rule. - */ -import { test } from "node:test"; -import { rules, RULE_IDS } from "../../eslint.ts"; -import { runESLintTests } from "../../lib/test-eslint.ts"; -import { - createRequiredInvalidCases, - createRequiredValidCases, -} from "../../lib/test-templates-eslint.ts"; - -const ruleName = RULE_IDS.actorFollowingPropertyRequired; -const rule = rules[ruleName]; - -test(`ESLint: ${ruleName}`, () => { - runESLintTests(ruleName, rule, { - valid: createRequiredValidCases("following"), - invalid: createRequiredInvalidCases("following", "required"), - }); -}); diff --git a/packages/lint/src/tests/eslint/actor-id-mismatch.test.ts b/packages/lint/src/tests/eslint/actor-id-mismatch.test.ts deleted file mode 100644 index baa45e913..000000000 --- a/packages/lint/src/tests/eslint/actor-id-mismatch.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * ESLint tests for actor-id-mismatch rule. - */ -import { test } from "node:test"; -import { rules, RULE_IDS } from "../../eslint.ts"; -import { runESLintTests } from "../../lib/test-eslint.ts"; -import { - createMismatchInvalidCases, - createMismatchValidCases, -} from "../../lib/test-templates-eslint.ts"; - -const ruleName = RULE_IDS.actorIdMismatch; -const rule = rules[ruleName]; - -test(`ESLint: ${ruleName}`, () => { - runESLintTests(ruleName, rule, { - valid: createMismatchValidCases("id"), - invalid: createMismatchInvalidCases("id", "mismatch"), - }); -}); diff --git a/packages/lint/src/tests/eslint/actor-id-required.test.ts b/packages/lint/src/tests/eslint/actor-id-required.test.ts deleted file mode 100644 index 069b4289e..000000000 --- a/packages/lint/src/tests/eslint/actor-id-required.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * ESLint tests for actor-id-required rule. - */ -import { test } from "node:test"; -import { rules, RULE_IDS } from "../../eslint.ts"; -import { runESLintTests } from "../../lib/test-eslint.ts"; -import { - createRequiredInvalidCases, - createRequiredValidCases, -} from "../../lib/test-templates-eslint.ts"; - -const ruleName = RULE_IDS.actorIdRequired; -const rule = rules[ruleName]; - -test(`ESLint: ${ruleName}`, () => { - runESLintTests(ruleName, rule, { - valid: createRequiredValidCases("id"), - invalid: createRequiredInvalidCases("id", "required"), - }); -}); diff --git a/packages/lint/src/tests/eslint/actor-inbox-property-mismatch.test.ts b/packages/lint/src/tests/eslint/actor-inbox-property-mismatch.test.ts deleted file mode 100644 index b391fe2f6..000000000 --- a/packages/lint/src/tests/eslint/actor-inbox-property-mismatch.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * ESLint tests for actor-inbox-property-mismatch rule. - */ -import { test } from "node:test"; -import { rules, RULE_IDS } from "../../eslint.ts"; -import { runESLintTests } from "../../lib/test-eslint.ts"; -import { - createMismatchInvalidCases, - createMismatchValidCases, -} from "../../lib/test-templates-eslint.ts"; - -const ruleName = RULE_IDS.actorInboxPropertyMismatch; -const rule = rules[ruleName]; - -test(`ESLint: ${ruleName}`, () => { - runESLintTests(ruleName, rule, { - valid: createMismatchValidCases("inbox"), - invalid: createMismatchInvalidCases("inbox", "mismatch"), - }); -}); diff --git a/packages/lint/src/tests/eslint/actor-inbox-property-required.test.ts b/packages/lint/src/tests/eslint/actor-inbox-property-required.test.ts deleted file mode 100644 index edfacfd09..000000000 --- a/packages/lint/src/tests/eslint/actor-inbox-property-required.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * ESLint tests for actor-inbox-property-required rule. - */ -import { test } from "node:test"; -import { rules, RULE_IDS } from "../../eslint.ts"; -import { runESLintTests } from "../../lib/test-eslint.ts"; -import { - createRequiredInvalidCases, - createRequiredValidCases, -} from "../../lib/test-templates-eslint.ts"; - -const ruleName = RULE_IDS.actorInboxPropertyRequired; -const rule = rules[ruleName]; - -test(`ESLint: ${ruleName}`, () => { - runESLintTests(ruleName, rule, { - valid: createRequiredValidCases("inbox"), - invalid: createRequiredInvalidCases("inbox", "required"), - }); -}); diff --git a/packages/lint/src/tests/eslint/actor-liked-property-mismatch.test.ts b/packages/lint/src/tests/eslint/actor-liked-property-mismatch.test.ts deleted file mode 100644 index ca86775ca..000000000 --- a/packages/lint/src/tests/eslint/actor-liked-property-mismatch.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * ESLint tests for actor-liked-property-mismatch rule. - */ -import { test } from "node:test"; -import { rules, RULE_IDS } from "../../eslint.ts"; -import { runESLintTests } from "../../lib/test-eslint.ts"; -import { - createMismatchInvalidCases, - createMismatchValidCases, -} from "../../lib/test-templates-eslint.ts"; - -const ruleName = RULE_IDS.actorLikedPropertyMismatch; -const rule = rules[ruleName]; - -test(`ESLint: ${ruleName}`, () => { - runESLintTests(ruleName, rule, { - valid: createMismatchValidCases("liked"), - invalid: createMismatchInvalidCases("liked", "mismatch"), - }); -}); diff --git a/packages/lint/src/tests/eslint/actor-liked-property-required.test.ts b/packages/lint/src/tests/eslint/actor-liked-property-required.test.ts deleted file mode 100644 index dad682ee3..000000000 --- a/packages/lint/src/tests/eslint/actor-liked-property-required.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * ESLint tests for actor-liked-property-required rule. - */ -import { test } from "node:test"; -import { rules, RULE_IDS } from "../../eslint.ts"; -import { runESLintTests } from "../../lib/test-eslint.ts"; -import { - createRequiredInvalidCases, - createRequiredValidCases, -} from "../../lib/test-templates-eslint.ts"; - -const ruleName = RULE_IDS.actorLikedPropertyRequired; -const rule = rules[ruleName]; - -test(`ESLint: ${ruleName}`, () => { - runESLintTests(ruleName, rule, { - valid: createRequiredValidCases("liked"), - invalid: createRequiredInvalidCases("liked", "required"), - }); -}); diff --git a/packages/lint/src/tests/eslint/actor-outbox-property-mismatch.test.ts b/packages/lint/src/tests/eslint/actor-outbox-property-mismatch.test.ts deleted file mode 100644 index c6bb73f6e..000000000 --- a/packages/lint/src/tests/eslint/actor-outbox-property-mismatch.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * ESLint tests for actor-outbox-property-mismatch rule. - */ -import { test } from "node:test"; -import { rules, RULE_IDS } from "../../eslint.ts"; -import { runESLintTests } from "../../lib/test-eslint.ts"; -import { - createMismatchInvalidCases, - createMismatchValidCases, -} from "../../lib/test-templates-eslint.ts"; - -const ruleName = RULE_IDS.actorOutboxPropertyMismatch; -const rule = rules[ruleName]; - -test(`ESLint: ${ruleName}`, () => { - runESLintTests(ruleName, rule, { - valid: createMismatchValidCases("outbox"), - invalid: createMismatchInvalidCases("outbox", "mismatch"), - }); -}); diff --git a/packages/lint/src/tests/eslint/actor-outbox-property-required.test.ts b/packages/lint/src/tests/eslint/actor-outbox-property-required.test.ts deleted file mode 100644 index a2c062b50..000000000 --- a/packages/lint/src/tests/eslint/actor-outbox-property-required.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * ESLint tests for actor-outbox-property-required rule. - */ -import { test } from "node:test"; -import { rules, RULE_IDS } from "../../eslint.ts"; -import { runESLintTests } from "../../lib/test-eslint.ts"; -import { - createRequiredInvalidCases, - createRequiredValidCases, -} from "../../lib/test-templates-eslint.ts"; - -const ruleName = RULE_IDS.actorOutboxPropertyRequired; -const rule = rules[ruleName]; - -test(`ESLint: ${ruleName}`, () => { - runESLintTests(ruleName, rule, { - valid: createRequiredValidCases("outbox"), - invalid: createRequiredInvalidCases("outbox", "required"), - }); -}); diff --git a/packages/lint/src/tests/eslint/actor-public-key-required.test.ts b/packages/lint/src/tests/eslint/actor-public-key-required.test.ts deleted file mode 100644 index 0b68c78e4..000000000 --- a/packages/lint/src/tests/eslint/actor-public-key-required.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * ESLint tests for actor-public-key-required rule. - */ -import { test } from "node:test"; -import { rules, RULE_IDS } from "../../eslint.ts"; -import { runESLintTests } from "../../lib/test-eslint.ts"; -import { - createRequiredInvalidCases, - createRequiredValidCases, -} from "../../lib/test-templates-eslint.ts"; - -const ruleName = RULE_IDS.actorPublicKeyRequired; -const rule = rules[ruleName]; - -test(`ESLint: ${ruleName}`, () => { - runESLintTests(ruleName, rule, { - valid: createRequiredValidCases("publicKey"), - invalid: createRequiredInvalidCases("publicKey", "required"), - }); -}); diff --git a/packages/lint/src/tests/eslint/actor-shared-inbox-property-mismatch.test.ts b/packages/lint/src/tests/eslint/actor-shared-inbox-property-mismatch.test.ts deleted file mode 100644 index 6049d859e..000000000 --- a/packages/lint/src/tests/eslint/actor-shared-inbox-property-mismatch.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * ESLint tests for actor-shared-inbox-property-mismatch rule. - */ -import { test } from "node:test"; -import { rules, RULE_IDS } from "../../eslint.ts"; -import { runESLintTests } from "../../lib/test-eslint.ts"; -import { - createMismatchInvalidCases, - createMismatchValidCases, -} from "../../lib/test-templates-eslint.ts"; - -const ruleName = RULE_IDS.actorSharedInboxPropertyMismatch; -const rule = rules[ruleName]; - -test(`ESLint: ${ruleName}`, () => { - runESLintTests(ruleName, rule, { - valid: createMismatchValidCases("sharedInbox"), - invalid: createMismatchInvalidCases("sharedInbox", "mismatch"), - }); -}); diff --git a/packages/lint/src/tests/eslint/actor-shared-inbox-property-required.test.ts b/packages/lint/src/tests/eslint/actor-shared-inbox-property-required.test.ts deleted file mode 100644 index 427768894..000000000 --- a/packages/lint/src/tests/eslint/actor-shared-inbox-property-required.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * ESLint tests for actor-shared-inbox-property-required rule. - */ -import { test } from "node:test"; -import { rules, RULE_IDS } from "../../eslint.ts"; -import { runESLintTests } from "../../lib/test-eslint.ts"; -import { - createRequiredInvalidCases, - createRequiredValidCases, -} from "../../lib/test-templates-eslint.ts"; - -const ruleName = RULE_IDS.actorSharedInboxPropertyRequired; -const rule = rules[ruleName]; - -test(`ESLint: ${ruleName}`, () => { - runESLintTests(ruleName, rule, { - valid: createRequiredValidCases("sharedInbox"), - invalid: createRequiredInvalidCases("sharedInbox", "required"), - }); -}); diff --git a/packages/lint/src/tests/eslint/collection-filtering-not-implemented.test.ts b/packages/lint/src/tests/eslint/collection-filtering-not-implemented.test.ts deleted file mode 100644 index ec1a62315..000000000 --- a/packages/lint/src/tests/eslint/collection-filtering-not-implemented.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * ESLint tests for collection-filtering-not-implemented rule. - */ -import { test } from "node:test"; -import { rules, RULE_IDS } from "../../eslint.ts"; -import { runESLintTests } from "../../lib/test-eslint.ts"; -import { - createCollectionFilteringInvalidCases, - createCollectionFilteringValidCases, -} from "../../lib/test-templates-eslint.ts"; - -const ruleName = RULE_IDS.collectionFilteringNotImplemented; -const rule = rules[ruleName]; - -test(`ESLint: ${ruleName}`, () => { - runESLintTests(ruleName, rule, { - valid: createCollectionFilteringValidCases(), - invalid: createCollectionFilteringInvalidCases("filterRequired"), - }); -}); diff --git a/packages/lint/src/tests/integration.test.ts b/packages/lint/src/tests/integration.test.ts index 290e45436..abf5dfc5b 100644 --- a/packages/lint/src/tests/integration.test.ts +++ b/packages/lint/src/tests/integration.test.ts @@ -5,7 +5,7 @@ * All tests start from the complete valid code and modify only the part * necessary to trigger the specific lint rule being tested. */ -import { assert, assertEquals } from "jsr:@std/assert"; +import { equal, ok } from "node:assert/strict"; import { test } from "node:test"; import plugin from "../mod.ts"; @@ -23,7 +23,7 @@ function lintCode(code: string): Deno.lint.Diagnostic[] { */ function assertNoErrors(code: string, message?: string) { const diagnostics = lintCode(code); - assertEquals( + equal( diagnostics.length, 0, message ?? @@ -40,7 +40,7 @@ function assertHasError(code: string, ruleName: string, message?: string) { const diagnostics = lintCode(code); const ruleId = `${PLUGIN_NAME}/${ruleName}`; const matched = diagnostics.some((d) => d.id === ruleId); - assert( + ok( matched, message ?? `Expected error from ${ruleName} but got: ${ @@ -51,10 +51,6 @@ function assertHasError(code: string, ruleName: string, message?: string) { ); } -// ============================================================================= -// Complete Valid Code (based on examples/lint/deno/mod.ts) -// ============================================================================= - /** * Complete valid code that passes all lint rules. * This is the baseline for all tests - each test modifies only what's needed. @@ -141,18 +137,10 @@ federation.setFeaturedTagsDispatcher( ); `; -// ============================================================================= -// Test: Complete Valid Code -// ============================================================================= - test("Integration: ✅ Complete valid code passes all rules", () => { assertNoErrors(COMPLETE_VALID_CODE); }); -// ============================================================================= -// Test: *-required rules (property missing when dispatcher is configured) -// ============================================================================= - test("Integration: ❌ actor-id-required - missing id property", () => { const code = COMPLETE_VALID_CODE.replace( "id: ctx.getActorUri(identifier),", @@ -526,11 +514,11 @@ test("Integration: ❌ Multiple errors - missing id and inbox", () => { const diagnostics = lintCode(code); const ruleIds = diagnostics.map((d) => d.id); - assert( + ok( ruleIds.includes(`${PLUGIN_NAME}/actor-id-required`), "Expected actor-id-required error", ); - assert( + ok( ruleIds.includes(`${PLUGIN_NAME}/actor-inbox-property-required`), "Expected actor-inbox-property-required error", ); From db96ce0b1c924793393599749942d6653cdfc210 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Sat, 13 Dec 2025 20:00:47 +0000 Subject: [PATCH 26/41] Removed unused files --- cspell.json | 1 + packages/lint/deno.json | 8 +- packages/lint/package.json | 8 +- packages/lint/src/eslint-rules.ts | 360 --------- packages/lint/src/eslint.ts | 199 ----- packages/lint/src/lib/ast-types.ts | 51 -- packages/lint/src/lib/common-pred.ts | 131 --- packages/lint/src/lib/common-tracker.ts | 59 -- packages/lint/src/lib/eslint-rule-factory.ts | 754 ------------------ packages/lint/src/lib/eslint-types.ts | 141 ---- .../lint/src/lib/property-checker-eslint.ts | 283 ------- packages/lint/src/lib/test-eslint.ts | 89 --- .../lint/src/lib/test-templates-eslint.ts | 382 --------- 13 files changed, 8 insertions(+), 2458 deletions(-) delete mode 100644 packages/lint/src/eslint-rules.ts delete mode 100644 packages/lint/src/eslint.ts delete mode 100644 packages/lint/src/lib/ast-types.ts delete mode 100644 packages/lint/src/lib/common-pred.ts delete mode 100644 packages/lint/src/lib/common-tracker.ts delete mode 100644 packages/lint/src/lib/eslint-rule-factory.ts delete mode 100644 packages/lint/src/lib/eslint-types.ts delete mode 100644 packages/lint/src/lib/property-checker-eslint.ts delete mode 100644 packages/lint/src/lib/test-eslint.ts delete mode 100644 packages/lint/src/lib/test-templates-eslint.ts diff --git a/cspell.json b/cspell.json index fbc4eb7d9..7702896ea 100644 --- a/cspell.json +++ b/cspell.json @@ -104,6 +104,7 @@ "traceparent", "ts-nocheck", "tsdown", + "TSES", "twoslash", "typeof", "unfollow", diff --git a/packages/lint/deno.json b/packages/lint/deno.json index a31a1d85c..6f2901518 100644 --- a/packages/lint/deno.json +++ b/packages/lint/deno.json @@ -3,11 +3,13 @@ "version": "2.0.0", "license": "MIT", "exports": { - ".": "./src/mod.ts", - "./eslint": "./src/eslint.ts" + ".": "./src/mod.ts" }, "imports": { - "@typescript-eslint/utils": "npm:@typescript-eslint/utils@^8.0.0" + "@types/eslint": "npm:@types/eslint@^9.0.0", + "@typescript-eslint/rule-tester": "npm:@typescript-eslint/rule-tester@^8.49.0", + "@typescript-eslint/utils": "npm:@typescript-eslint/utils@^8.49.0", + "eslint": "npm:eslint@^9.0.0" }, "tasks": { "test": "deno test --allow-all" diff --git a/packages/lint/package.json b/packages/lint/package.json index 0f6a51258..fd5cdf558 100644 --- a/packages/lint/package.json +++ b/packages/lint/package.json @@ -38,10 +38,6 @@ "import": "./dist/index.js", "require": "./dist/index.cjs" }, - "./eslint": { - "import": "./dist/eslint.js", - "require": "./dist/eslint.cjs" - }, "./package.json": "./package.json" }, "files": [ @@ -50,7 +46,7 @@ ], "peerDependencies": { "@fedify/fedify": "workspace:^", - "eslint": ">=8.0.0" + "eslint": ">=9.0.0" }, "peerDependenciesMeta": { "eslint": { @@ -71,6 +67,6 @@ "build": "tsdown", "prepack": "tsdown", "prepublish": "tsdown", - "test": "tsdown && node --experimental-transform-types --test src/tests/eslint/*.test.ts" + "test": "node --experimental-transform-types" } } diff --git a/packages/lint/src/eslint-rules.ts b/packages/lint/src/eslint-rules.ts deleted file mode 100644 index f2cc0f46f..000000000 --- a/packages/lint/src/eslint-rules.ts +++ /dev/null @@ -1,360 +0,0 @@ -/** - * ESLint rule factories for Fedify lint rules. - * Uses TSESTree types from @typescript-eslint/utils. - */ -import type { TSESLint, TSESTree } from "@typescript-eslint/utils"; -import { - actorPropertyMismatch, - actorPropertyRequired, - COLLECTION_FILTERING_NOT_IMPLEMENTED_ERROR, -} from "./lib/messages.ts"; -import { - createPropertyExistenceChecker, - createPropertyValueChecker, - extractFunctionParams, - hasMinParams, - searchFunctionBody, -} from "./lib/property-checker-eslint.ts"; -import type { MethodCallContext, PropertyConfig } from "./lib/types.ts"; - -// ============================================================================ -// Types -// ============================================================================ - -type RuleContext = TSESLint.RuleContext; -type RuleModule = TSESLint.RuleModule; -type CallExpression = TSESTree.CallExpression; -type MemberExpression = TSESTree.MemberExpression; -type Identifier = TSESTree.Identifier; -type VariableDeclarator = TSESTree.VariableDeclarator; -type Expression = TSESTree.Expression; -type FunctionNode = - | TSESTree.ArrowFunctionExpression - | TSESTree.FunctionExpression; - -// ============================================================================ -// Federation Variable Tracker -// ============================================================================ - -const isCreateFederationCall = (node: CallExpression): boolean => - node.callee.type === "Identifier" && - /^create(Federation|FederationBuilder)$/i.test(node.callee.name); - -interface FederationTracker { - handleVariableDeclarator(node: VariableDeclarator): void; - isFederationObject(node: Expression): boolean; -} - -function createFederationTracker(): FederationTracker { - const federationVariables = new Set(); - - const isFederationObject = (node: Expression): boolean => { - switch (node.type) { - case "Identifier": - return federationVariables.has(node.name); - case "CallExpression": - if (isCreateFederationCall(node)) return true; - if (node.callee.type === "MemberExpression") { - return isFederationObject(node.callee.object); - } - return false; - case "MemberExpression": - return isFederationObject(node.object); - default: - return false; - } - }; - - return { - handleVariableDeclarator(node: VariableDeclarator): void { - if ( - node.init?.type === "CallExpression" && - isCreateFederationCall(node.init) && - node.id.type === "Identifier" - ) { - federationVariables.add(node.id.name); - } - }, - isFederationObject, - }; -} - -// ============================================================================ -// CallExpression Helpers -// ============================================================================ - -interface CallMemberExpression extends CallExpression { - callee: MemberExpression & { property: Identifier }; -} - -function isCallMemberExpression( - node: CallExpression, -): node is CallMemberExpression { - return ( - node.callee.type === "MemberExpression" && - node.callee.property.type === "Identifier" - ); -} - -function hasMethodName( - node: CallMemberExpression, - name: string, -): boolean { - return node.callee.property.name === name; -} - -function isSetActorDispatcherCall( - node: CallExpression, -): node is CallMemberExpression { - return ( - isCallMemberExpression(node) && - hasMethodName(node, "setActorDispatcher") && - node.arguments.length >= 2 - ); -} - -function isSetFollowersDispatcherCall( - node: CallExpression, -): node is CallMemberExpression { - return ( - isCallMemberExpression(node) && - hasMethodName(node, "setFollowersDispatcher") && - node.arguments.length >= 2 - ); -} - -function isFunction(node: TSESTree.Node): node is FunctionNode { - return ( - node.type === "ArrowFunctionExpression" || - node.type === "FunctionExpression" - ); -} - -// ============================================================================ -// Dispatcher Tracker -// ============================================================================ - -interface DispatcherTracker { - isConfigured(): boolean; - checkCall(node: CallExpression, tracker: FederationTracker): void; -} - -function createDispatcherTracker(methodName: string): DispatcherTracker { - let configured = false; - - return { - isConfigured: () => configured, - checkCall(node: CallExpression, tracker: FederationTracker): void { - if ( - isCallMemberExpression(node) && - hasMethodName(node, methodName) && - tracker.isFederationObject(node.callee.object) - ) { - configured = true; - } - }, - }; -} - -// ============================================================================ -// Actor Dispatcher Info -// ============================================================================ - -interface ActorDispatcherInfo { - node: CallMemberExpression; - dispatcherFn: FunctionNode; -} - -// ============================================================================ -// ESLint Rule Factory: Required Rules -// ============================================================================ - -export function createRequiredRule( - _ruleId: string, - config: PropertyConfig, -): RuleModule { - return { - meta: { - type: "suggestion", - docs: { - description: `Ensure actor dispatcher returns ${ - config.path.join(".") - } property`, - }, - schema: [], - messages: { - required: "{{ message }}", - }, - }, - defaultOptions: [], - create(context: RuleContext) { - const federationTracker = createFederationTracker(); - const dispatcherTracker = createDispatcherTracker(config.setter); - const actorDispatchers: ActorDispatcherInfo[] = []; - - const propertyChecker = createPropertyExistenceChecker(config.path); - - return { - VariableDeclarator(node: VariableDeclarator): void { - federationTracker.handleVariableDeclarator(node); - }, - - CallExpression(node: CallExpression): void { - dispatcherTracker.checkCall(node, federationTracker); - - if (!isSetActorDispatcherCall(node)) return; - if (!federationTracker.isFederationObject(node.callee.object)) return; - - const dispatcherArg = node.arguments[1]; - if (isFunction(dispatcherArg)) { - actorDispatchers.push({ - node, - dispatcherFn: dispatcherArg, - }); - } - }, - - "Program:exit"(): void { - if (!dispatcherTracker.isConfigured()) return; - - for (const { dispatcherFn } of actorDispatchers) { - const hasProperty = searchFunctionBody(propertyChecker)( - dispatcherFn.body, - ); - - if (!hasProperty) { - context.report({ - node: dispatcherFn, - messageId: "required", - data: { message: actorPropertyRequired(config) }, - }); - } - } - }, - }; - }, - }; -} - -// ============================================================================ -// ESLint Rule Factory: Mismatch Rules -// ============================================================================ - -export function createMismatchRule( - _ruleId: string, - config: PropertyConfig, -): RuleModule { - return { - meta: { - type: "problem", - docs: { - description: `Ensure actor's ${ - config.path.join(".") - } property uses correct context method`, - }, - schema: [], - messages: { - mismatch: "{{ message }}", - }, - }, - defaultOptions: [], - create(context: RuleContext) { - const federationTracker = createFederationTracker(); - - return { - VariableDeclarator(node: VariableDeclarator): void { - federationTracker.handleVariableDeclarator(node); - }, - - CallExpression(node: CallExpression): void { - if (!isSetActorDispatcherCall(node)) return; - if (!federationTracker.isFederationObject(node.callee.object)) return; - - const dispatcherArg = node.arguments[1]; - if (!isFunction(dispatcherArg)) return; - - const [ctxName, idName] = extractFunctionParams(dispatcherArg); - if (!ctxName || !idName) return; - - const methodCallContext: MethodCallContext = { - path: config.path.join("."), - ctxName, - idName, - methodName: config.getter, - requiresIdentifier: config.requiresIdentifier, - }; - - // Check if property exists - const existenceChecker = createPropertyExistenceChecker(config.path); - const hasProperty = searchFunctionBody(existenceChecker)( - dispatcherArg.body, - ); - - if (!hasProperty) return; // Let required rule handle this - - // Check if property has correct value - const valueChecker = createPropertyValueChecker( - config.path, - methodCallContext, - ); - const hasCorrectValue = searchFunctionBody(valueChecker)( - dispatcherArg.body, - ); - - if (!hasCorrectValue) { - context.report({ - node: dispatcherArg, - messageId: "mismatch", - data: { message: actorPropertyMismatch(methodCallContext) }, - }); - } - }, - }; - }, - }; -} - -// ============================================================================ -// ESLint Rule Factory: Collection Filtering -// ============================================================================ - -export function createCollectionFilteringRule(_ruleId: string): RuleModule { - return { - meta: { - type: "suggestion", - docs: { - description: "Ensure followers dispatcher implements filtering", - }, - schema: [], - messages: { - filterRequired: COLLECTION_FILTERING_NOT_IMPLEMENTED_ERROR, - }, - }, - defaultOptions: [], - create(context: RuleContext) { - const federationTracker = createFederationTracker(); - - return { - VariableDeclarator(node: VariableDeclarator): void { - federationTracker.handleVariableDeclarator(node); - }, - - CallExpression(node: CallExpression): void { - if (!isSetFollowersDispatcherCall(node)) return; - if (!federationTracker.isFederationObject(node.callee.object)) return; - - const dispatcherArg = node.arguments[1]; - if (!isFunction(dispatcherArg)) return; - - // Filter is the 4th parameter (index 3) - if (!hasMinParams(4)(dispatcherArg)) { - context.report({ - node: dispatcherArg, - messageId: "filterRequired", - }); - } - }, - }; - }, - }; -} diff --git a/packages/lint/src/eslint.ts b/packages/lint/src/eslint.ts deleted file mode 100644 index 7ae556a66..000000000 --- a/packages/lint/src/eslint.ts +++ /dev/null @@ -1,199 +0,0 @@ -/** - * ESLint plugin for Fedify. - * Provides lint rules for validating Fedify federation code. - */ -import { pipe } from "@fxts/core"; -import type { TSESLint } from "@typescript-eslint/utils"; -import { - createCollectionFilteringRule, - createMismatchRule, - createRequiredRule, -} from "./eslint-rules.ts"; -import { properties } from "./lib/const.ts"; - -// ============================================================================ -// Rule IDs -// ============================================================================ - -const RULE_IDS = { - // Required rules - actorIdRequired: "actor-id-required", - actorFollowingPropertyRequired: "actor-following-property-required", - actorFollowersPropertyRequired: "actor-followers-property-required", - actorOutboxPropertyRequired: "actor-outbox-property-required", - actorLikedPropertyRequired: "actor-liked-property-required", - actorFeaturedPropertyRequired: "actor-featured-property-required", - actorFeaturedTagsPropertyRequired: "actor-featured-tags-property-required", - actorInboxPropertyRequired: "actor-inbox-property-required", - actorSharedInboxPropertyRequired: "actor-shared-inbox-property-required", - actorPublicKeyRequired: "actor-public-key-required", - actorAssertionMethodRequired: "actor-assertion-method-required", - - // Mismatch rules - actorIdMismatch: "actor-id-mismatch", - actorFollowingPropertyMismatch: "actor-following-property-mismatch", - actorFollowersPropertyMismatch: "actor-followers-property-mismatch", - actorOutboxPropertyMismatch: "actor-outbox-property-mismatch", - actorLikedPropertyMismatch: "actor-liked-property-mismatch", - actorFeaturedPropertyMismatch: "actor-featured-property-mismatch", - actorFeaturedTagsPropertyMismatch: "actor-featured-tags-property-mismatch", - actorInboxPropertyMismatch: "actor-inbox-property-mismatch", - actorSharedInboxPropertyMismatch: "actor-shared-inbox-property-mismatch", - - // Collection rules - collectionFilteringNotImplemented: "collection-filtering-not-implemented", -} as const; - -// ============================================================================ -// Rule Definitions -// ============================================================================ - -const rules: Record> = { - // Required rules - [RULE_IDS.actorIdRequired]: createRequiredRule( - RULE_IDS.actorIdRequired, - properties.id, - ), - [RULE_IDS.actorFollowingPropertyRequired]: createRequiredRule( - RULE_IDS.actorFollowingPropertyRequired, - properties.following, - ), - [RULE_IDS.actorFollowersPropertyRequired]: createRequiredRule( - RULE_IDS.actorFollowersPropertyRequired, - properties.followers, - ), - [RULE_IDS.actorOutboxPropertyRequired]: createRequiredRule( - RULE_IDS.actorOutboxPropertyRequired, - properties.outbox, - ), - [RULE_IDS.actorLikedPropertyRequired]: createRequiredRule( - RULE_IDS.actorLikedPropertyRequired, - properties.liked, - ), - [RULE_IDS.actorFeaturedPropertyRequired]: createRequiredRule( - RULE_IDS.actorFeaturedPropertyRequired, - properties.featured, - ), - [RULE_IDS.actorFeaturedTagsPropertyRequired]: createRequiredRule( - RULE_IDS.actorFeaturedTagsPropertyRequired, - properties.featuredTags, - ), - [RULE_IDS.actorInboxPropertyRequired]: createRequiredRule( - RULE_IDS.actorInboxPropertyRequired, - properties.inbox, - ), - [RULE_IDS.actorSharedInboxPropertyRequired]: createRequiredRule( - RULE_IDS.actorSharedInboxPropertyRequired, - properties.sharedInbox, - ), - [RULE_IDS.actorPublicKeyRequired]: createRequiredRule( - RULE_IDS.actorPublicKeyRequired, - properties.publicKey, - ), - [RULE_IDS.actorAssertionMethodRequired]: createRequiredRule( - RULE_IDS.actorAssertionMethodRequired, - properties.assertionMethod, - ), - - // Mismatch rules - [RULE_IDS.actorIdMismatch]: createMismatchRule( - RULE_IDS.actorIdMismatch, - properties.id, - ), - [RULE_IDS.actorFollowingPropertyMismatch]: createMismatchRule( - RULE_IDS.actorFollowingPropertyMismatch, - properties.following, - ), - [RULE_IDS.actorFollowersPropertyMismatch]: createMismatchRule( - RULE_IDS.actorFollowersPropertyMismatch, - properties.followers, - ), - [RULE_IDS.actorOutboxPropertyMismatch]: createMismatchRule( - RULE_IDS.actorOutboxPropertyMismatch, - properties.outbox, - ), - [RULE_IDS.actorLikedPropertyMismatch]: createMismatchRule( - RULE_IDS.actorLikedPropertyMismatch, - properties.liked, - ), - [RULE_IDS.actorFeaturedPropertyMismatch]: createMismatchRule( - RULE_IDS.actorFeaturedPropertyMismatch, - properties.featured, - ), - [RULE_IDS.actorFeaturedTagsPropertyMismatch]: createMismatchRule( - RULE_IDS.actorFeaturedTagsPropertyMismatch, - properties.featuredTags, - ), - [RULE_IDS.actorInboxPropertyMismatch]: createMismatchRule( - RULE_IDS.actorInboxPropertyMismatch, - properties.inbox, - ), - [RULE_IDS.actorSharedInboxPropertyMismatch]: createMismatchRule( - RULE_IDS.actorSharedInboxPropertyMismatch, - properties.sharedInbox, - ), - - // Collection rules - [RULE_IDS.collectionFilteringNotImplemented]: createCollectionFilteringRule( - RULE_IDS.collectionFilteringNotImplemented, - ), -}; - -// ============================================================================ -// Plugin Configuration -// ============================================================================ - -/** - * Recommended configuration - enables all rules as warnings - */ -/** - * Recommended configuration - enables all rules as warnings - */ -const recommendedRules = pipe( - Object.keys(rules), - (keys) => - keys.reduce((acc, key) => { - acc[`@fedify/lint/${key}`] = "warn" as const; - return acc; - }, {} as Record), -); - -/** - * Strict configuration - enables all rules as errors - */ -const strictRules = pipe( - Object.keys(rules), - (keys) => - keys.reduce((acc, key) => { - acc[`@fedify/lint/${key}`] = "error" as const; - return acc; - }, {} as Record), -); - -// ============================================================================ -// Plugin Export -// ============================================================================ - -const plugin: TSESLint.Linter.Plugin = { - meta: { - name: "@fedify/lint", - version: "2.0.0", - }, - rules, - configs: { - recommended: { - plugins: ["@fedify/lint"], - rules: recommendedRules, - }, - strict: { - plugins: ["@fedify/lint"], - rules: strictRules, - }, - }, -}; - -export default plugin; - -// Named exports for convenience -export { RULE_IDS, rules }; -export type { TSESLint }; diff --git a/packages/lint/src/lib/ast-types.ts b/packages/lint/src/lib/ast-types.ts deleted file mode 100644 index e2eaf7792..000000000 --- a/packages/lint/src/lib/ast-types.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Common AST type definitions for ESLint rules. - * Uses TSESTree types from @typescript-eslint/utils. - */ -import type { TSESTree } from "@typescript-eslint/utils"; - -// ============================================================================ -// Re-export TSESTree types with aliases for convenience -// ============================================================================ - -export type Node = TSESTree.Node; -export type Expression = TSESTree.Expression; -export type Statement = TSESTree.Statement; -export type Parameter = TSESTree.Parameter; -export type Pattern = TSESTree.BindingName; - -export type Identifier = TSESTree.Identifier; -export type MemberExpression = TSESTree.MemberExpression; -export type CallExpression = TSESTree.CallExpression; -export type ObjectExpression = TSESTree.ObjectExpression; -export type Property = TSESTree.Property; -export type NewExpression = TSESTree.NewExpression; -export type ConditionalExpression = TSESTree.ConditionalExpression; -export type SpreadElement = TSESTree.SpreadElement; -export type ArrowFunctionExpression = TSESTree.ArrowFunctionExpression; -export type FunctionExpression = TSESTree.FunctionExpression; -export type BlockStatement = TSESTree.BlockStatement; -export type ReturnStatement = TSESTree.ReturnStatement; -export type VariableDeclarator = TSESTree.VariableDeclarator; -export type Program = TSESTree.Program; - -// ============================================================================ -// Custom Union Types -// ============================================================================ - -export type FunctionNode = ArrowFunctionExpression | FunctionExpression; - -// ============================================================================ -// CallExpression with MemberExpression callee -// ============================================================================ - -export interface CallMemberExpression extends Omit { - callee: MemberExpression; -} - -export interface CallMemberExpressionWithIdentifier - extends Omit { - callee: MemberExpression & { - property: Identifier; - }; -} diff --git a/packages/lint/src/lib/common-pred.ts b/packages/lint/src/lib/common-pred.ts deleted file mode 100644 index 6dc7eebbc..000000000 --- a/packages/lint/src/lib/common-pred.ts +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Common predicate functions for AST node checking. - * These functions work with TSESTree AST nodes from @typescript-eslint/utils. - */ -import { pipe, prop } from "@fxts/core"; -import type { TSESTree } from "@typescript-eslint/utils"; -import type { - CallExpression, - CallMemberExpression, - CallMemberExpressionWithIdentifier, - FunctionNode, - Identifier, - MemberExpression, - Node, -} from "./ast-types.ts"; -import { eq } from "./utils.ts"; - -/** - * Combines multiple predicates with AND logic. - */ -export function allOf( - ...predicates: Array<(value: T) => boolean> -): (value: T) => boolean { - return (value: T): boolean => - predicates.every((predicate) => predicate(value)); -} - -/** - * Type guard to check if a value is a valid AST node. - */ -export const isNode = (value: unknown): value is Node => - typeof value === "object" && - value !== null && - "type" in value && - typeof (value as { type: unknown }).type === "string"; - -/** - * Checks if a node is of a specific type. - */ -export const isNodeType = - (type: T) => - (node: Node): node is Extract => node.type === type; - -/** - * Checks if a node has a specific name property (for Identifier nodes). - */ -export const isNodeName = - (name: T) => - (node: Identifier): node is Identifier & { name: T } => node.name === name; - -/** - * Type guard for Identifier node. - */ -export const isIdentifier = (node: Node): node is Identifier => - node.type === "Identifier"; - -/** - * Checks if a node's key is an Identifier (for Property nodes). - */ -export const hasIdentifierKey = ( - node: TSESTree.Property, -): node is TSESTree.Property & { key: Identifier } => - node.key.type === "Identifier"; - -/** - * Checks if a node's callee is a MemberExpression. - */ -export const hasMemberExpressionCallee = ( - node: CallExpression, -): node is CallMemberExpression => node.callee.type === "MemberExpression"; - -/** - * Checks if a node's callee property is an Identifier. - */ -export const hasIdentifierProperty = ( - node: CallMemberExpression, -): node is CallMemberExpressionWithIdentifier => - node.callee.property.type === "Identifier"; - -/** - * Checks if a node's callee property name matches the given method name. - */ -export const hasMethodName = - (methodName: T) => - (node: CallMemberExpressionWithIdentifier): boolean => - node.callee.property.name === methodName; - -/** - * Checks if a CallExpression has minimum required arguments. - */ -export const hasMinArguments = - (min: number) => (node: CallExpression): boolean => - node.arguments.length >= min; - -/** - * Checks if an expression is an arrow function. - */ -export const isArrowFunction = ( - node: Node, -): node is TSESTree.ArrowFunctionExpression => - node.type === "ArrowFunctionExpression"; - -/** - * Checks if an expression is a function expression. - */ -export const isFunctionExpression = ( - node: Node, -): node is TSESTree.FunctionExpression => node.type === "FunctionExpression"; - -/** - * Checks if an expression is a function (arrow or regular). - */ -export const isFunction = (node: Node): node is FunctionNode => - isArrowFunction(node) || isFunctionExpression(node); - -/** - * Checks if a CallExpression is a setActorDispatcher call with proper structure. - */ -export const isSetActorDispatcherCall = ( - node: CallExpression, -): node is CallMemberExpressionWithIdentifier & { - callee: MemberExpression & { - property: Identifier & { name: "setActorDispatcher" }; - }; -} => { - if (!hasMemberExpressionCallee(node)) return false; - if (!hasIdentifierProperty(node)) return false; - if (!hasMethodName("setActorDispatcher")(node)) return false; - if (!hasMinArguments(2)(node)) return false; - return true; -}; diff --git a/packages/lint/src/lib/common-tracker.ts b/packages/lint/src/lib/common-tracker.ts deleted file mode 100644 index 67d039953..000000000 --- a/packages/lint/src/lib/common-tracker.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Common tracker for Federation variable tracking. - * Works with TSESTree AST nodes from @typescript-eslint/utils. - */ -import type { TSESTree } from "@typescript-eslint/utils"; -import type { CallExpression, Node, VariableDeclarator } from "./ast-types.ts"; - -/** - * Checks if a CallExpression is a createFederation or createFederationBuilder call. - */ -const isCreateFederation = (node: CallExpression): boolean => - node.callee.type === "Identifier" && - /^create(Federation|FederationBuilder)$/i.test(node.callee.name); - -/** - * Helper to track variable names that store the result of createFederation() - * or createFederationBuilder() calls. - */ -export function trackFederationVariables() { - const federationVariables = new Set(); - - const isFederationObject = (obj: TSESTree.Expression): boolean => { - switch (obj.type) { - case "Identifier": - return federationVariables.has(obj.name); - case "CallExpression": { - // Check if it's a direct createFederation call - if (isCreateFederation(obj)) return true; - // Check if it's a chained method call on a federation object - if (obj.callee.type === "MemberExpression") { - return isFederationObject(obj.callee.object); - } - return false; - } - case "MemberExpression": - return isFederationObject(obj.object); - } - return false; - }; - - return { - VariableDeclarator(node: VariableDeclarator) { - const init = node.init; - const id = node.id; - - if (init?.type === "CallExpression") { - if (isCreateFederation(init) && id.type === "Identifier") { - federationVariables.add(id.name); - } - } - }, - - isFederationVariable(name: string): boolean { - return federationVariables.has(name); - }, - - isFederationObject, - }; -} diff --git a/packages/lint/src/lib/eslint-rule-factory.ts b/packages/lint/src/lib/eslint-rule-factory.ts deleted file mode 100644 index 52c028fd1..000000000 --- a/packages/lint/src/lib/eslint-rule-factory.ts +++ /dev/null @@ -1,754 +0,0 @@ -/** - * ESLint rule factory for creating Fedify lint rules. - * Adapts common rule logic to ESLint's API. - */ -import { - filter, - isArray, - isEmpty, - isNil, - isObject, - negate, - pipe, - pipeLazy, - prop, - some, - toArray, -} from "@fxts/core"; -import type { TSESLint, TSESTree } from "@typescript-eslint/utils"; -import type { ASTNode, CallExpression, FunctionNode } from "./ast-types.ts"; -import { - allOf, - hasIdentifierKey, - hasIdentifierProperty, - hasMemberExpressionCallee, - hasMethodName, - hasMinArguments, - isASTNode, - isFunction, - isNodeName, - isNodeType, - isSetActorDispatcherCall, -} from "./common-pred.ts"; -import { trackFederationVariables } from "./common-tracker.ts"; -import { - actorPropertyMismatch, - actorPropertyRequired, - COLLECTION_FILTERING_NOT_IMPLEMENTED_ERROR, -} from "./messages.ts"; -import type { MethodCallContext, PropertyConfig } from "./types.ts"; -import { eq } from "./utils.ts"; - -// ============================================================================ -// Types -// ============================================================================ - -type ESLintRuleContext = TSESLint.RuleContext; - -interface ActorDispatcherInfo { - node: CallExpression; - dispatcherArg: FunctionNode; -} - -// ============================================================================ -// Property Checkers -// ============================================================================ - -/** - * Checks if a value is a valid AST node object. - */ -const isASTNodeObj = (node: unknown): node is ASTNode => - isObject(node) && !isNil(node); - -/** - * Filters and casts array items to ASTNode. - */ -const filterASTNodes = (items: unknown[]): ASTNode[] => - pipe( - items, - filter(isObject), - filter((p): p is ASTNode => !isNil(p)), - toArray, - ); - -const isIdentifierWithName = - (name: T) => - (node: N): node is N & { type: "Identifier"; name: T } => - allOf(isNodeType("Identifier"), isNodeName(name))(node); - -const isPropertyWithKeyName = (path: string) => -( - node: ASTNode, -): node is { type: "Property"; key: { name: string } & ASTNode } & ASTNode => - allOf( - isNodeType("Property"), - hasIdentifierKey, - pipeLazy( - prop("key")<{ key: { name: string } }>, - prop("name"), - eq(path), - ) as (value: unknown) => boolean, - )(node); - -// ============================================================================ -// Required Rule Property Checker -// ============================================================================ - -/** - * Creates a predicate function that checks if a property has a specific name. - */ -const createPropertyChecker = - (propertyName: T) => (node: unknown): boolean => - isASTNode(node) && isPropertyWithKeyName(propertyName)(node); - -/** - * Internal recursive checker for nested property paths. - */ -const checkNestedPropertyPath = - (path: readonly string[]) => (node: unknown): boolean => { - if (!isASTNode(node) || !isPropertyWithKeyName(path[0])(node)) return false; - if (path.length === 1) return true; - - const value = (node as { value: ASTNode }).value; - - // Handle ObjectExpression: endpoints: { sharedInbox: ... } - if (isNodeType("ObjectExpression")(value)) { - return pipe( - (value as { properties: unknown[] }).properties, - filterASTNodes, - some(checkNestedPropertyPath(path.slice(1))), - ); - } - - // Handle NewExpression: endpoints: new Endpoints({ sharedInbox: ... }) - if (isNodeType("NewExpression")(value)) { - const args = (value as { arguments: unknown[] }).arguments; - if (!isArray(args) || args.length === 0) return false; - const firstArg = args[0]; - if ( - !isASTNodeObj(firstArg) || !isNodeType("ObjectExpression")(firstArg) - ) { - return false; - } - return pipe( - (firstArg as { properties: unknown[] }).properties, - filterASTNodes, - some(checkNestedPropertyPath(path.slice(1))), - ); - } - - return false; - }; - -/** - * Creates a predicate function that checks if a nested property exists. - */ -const createNestedPropertyChecker = - (path: readonly string[]) => (node: unknown): boolean => - checkNestedPropertyPath(path)(node); - -/** - * Checks if an ObjectExpression node contains a property. - */ -const checkObjectExpression = - (propertyChecker: (prop: unknown) => boolean) => (obj: ASTNode): boolean => - pipe( - obj as { properties: unknown[] }, - prop("properties"), - (properties) => - Array.isArray(properties) - ? pipe(properties, filter(isASTNode), some(propertyChecker)) - : false, - ); - -/** - * Extracts the first argument if it's an ObjectExpression. - */ -const extractFirstArgument = (node: ASTNode): ASTNode | null => - pipe( - node as { arguments: unknown[] }, - prop("arguments"), - (args) => { - if (!Array.isArray(args) || args.length === 0) return null; - const firstArg = args[0]; - return isASTNode(firstArg) && isNodeType("ObjectExpression")(firstArg) - ? firstArg - : null; - }, - ); - -/** - * Extracts ObjectExpression from NewExpression. - */ -const extractObjectExpression = (arg: ASTNode): ASTNode | null => { - if (isNodeType("NewExpression")(arg)) return extractFirstArgument(arg); - return null; -}; - -/** - * Checks if a ConditionalExpression has the property in both branches. - */ -const checkConditionalExpression = - (propertyChecker: (prop: unknown) => boolean) => (node: ASTNode): boolean => { - const consequent = (node as { consequent: ASTNode }).consequent; - const alternate = (node as { alternate: ASTNode }).alternate; - - const checkBranch = (branch: ASTNode): boolean => { - if (isNodeType("ConditionalExpression")(branch)) { - return checkConditionalExpression(propertyChecker)(branch); - } - const objExpr = extractObjectExpression(branch); - return objExpr ? checkObjectExpression(propertyChecker)(objExpr) : false; - }; - - return checkBranch(consequent) && checkBranch(alternate); - }; - -/** - * Checks if a ReturnStatement node contains a property. - */ -const checkReturnStatement = - (propertyChecker: (prop: unknown) => boolean) => (node: ASTNode): boolean => { - const arg = (node as { argument: ASTNode | null }).argument; - if (!arg || !isASTNode(arg)) return false; - - if (isNodeType("ConditionalExpression")(arg)) { - return checkConditionalExpression(propertyChecker)(arg); - } - - const objExpr = extractObjectExpression(arg); - return objExpr ? checkObjectExpression(propertyChecker)(objExpr) : false; - }; - -/** - * Creates a function that recursively checks for a property in an AST node. - */ -const createPropertySearcher = - (propertyChecker: (prop: unknown) => boolean) => (node: unknown): boolean => { - if (!isASTNode(node)) return false; - - if (isNodeType("ReturnStatement")(node)) { - return checkReturnStatement(propertyChecker)(node); - } - - if (isNodeType("BlockStatement")(node)) { - return (node as { body: unknown[] }).body.some( - createPropertySearcher(propertyChecker), - ); - } - - // Handle arrow function with direct NewExpression body - if (isNodeType("NewExpression")(node)) { - const objExpr = extractFirstArgument(node); - return objExpr ? checkObjectExpression(propertyChecker)(objExpr) : false; - } - - return false; - }; - -// ============================================================================ -// Mismatch Rule Value Checker -// ============================================================================ - -/** - * Checks if a node is a CallExpression calling the expected context method. - */ -const isExpectedMethodCall = ( - node: ASTNode, - { ctxName, idName, methodName, requiresIdentifier }: MethodCallContext, -): boolean => { - if ( - !isNodeType("CallExpression")(node) || - !hasMemberExpressionCallee(node as unknown as CallExpression) || - !isIdentifierWithName(ctxName)( - ((node as unknown as CallExpression).callee as { object: ASTNode }) - .object, - ) || - !isIdentifierWithName(methodName)( - ((node as unknown as CallExpression).callee as { property: ASTNode }) - .property, - ) - ) return false; - - const args = (node as unknown as CallExpression).arguments; - if (!requiresIdentifier) { - return isEmpty(args); - } - return !isEmpty(args) && some(isIdentifierWithName(idName))(args); -}; - -/** - * Creates a property existence checker for the given property path. - */ -const createPropertyExistenceChecker = (path: readonly string[]) => { - const checkPropertyExists = - (path: readonly string[]) => (node: ASTNode): boolean => { - if (!isPropertyWithKeyName(path[0])(node)) return false; - if (path.length === 1) return true; - - const value = (node as { value: ASTNode }).value; - - if (isNodeType("ObjectExpression")(value)) { - return pipe( - (value as { properties: unknown[] }).properties, - filterASTNodes, - some(checkPropertyExists(path.slice(1))), - ); - } - - if (isNodeType("NewExpression")(value)) { - const args = (value as { arguments: unknown[] }).arguments; - if (!isArray(args) || args.length === 0) return false; - const firstArg = args[0]; - if ( - !isASTNodeObj(firstArg) || !isNodeType("ObjectExpression")(firstArg) - ) { - return false; - } - return pipe( - (firstArg as { properties: unknown[] }).properties, - filterASTNodes, - some(checkPropertyExists(path.slice(1))), - ); - } - - return false; - }; - - return (prop: ASTNode): boolean => - allOf(isASTNodeObj, checkPropertyExists(path))(prop); -}; - -/** - * Creates a property value checker for the given property path. - */ -const createPropertyValueChecker = ( - path: readonly string[], - ctx: MethodCallContext, -) => { - const checkPropertyValue = - (path: readonly string[]) => (prop: ASTNode): boolean => { - if (!isPropertyWithKeyName(path[0])(prop)) return false; - - const value = (prop as { value: ASTNode }).value; - if (path.length === 1) { - return isExpectedMethodCall(value, ctx); - } - - if (isNodeType("ObjectExpression")(value)) { - return pipe( - (value as { properties: unknown[] }).properties, - filterASTNodes, - some(checkPropertyValue(path.slice(1))), - ); - } - - if (isNodeType("NewExpression")(value)) { - const args = (value as { arguments: unknown[] }).arguments; - if (!isArray(args) || args.length === 0) return false; - const firstArg = args[0]; - if ( - !isASTNodeObj(firstArg) || !isNodeType("ObjectExpression")(firstArg) - ) { - return false; - } - return pipe( - (firstArg as { properties: unknown[] }).properties, - filterASTNodes, - some(checkPropertyValue(path.slice(1))), - ); - } - - return false; - }; - - return (prop: ASTNode): boolean => - allOf(isASTNodeObj, checkPropertyValue(path))(prop); -}; - -/** - * Checks if a function body contains the correct property value. - */ -const checkFunctionBody = - (propertyChecker: (prop: ASTNode) => boolean) => (node: ASTNode): boolean => { - if (!isASTNodeObj(node)) return false; - - if (isNodeType("ReturnStatement")(node)) { - const arg = (node as { argument: ASTNode | null }).argument; - if (!arg || !isASTNodeObj(arg)) return false; - - if (isNodeType("ConditionalExpression")(arg)) { - const checkBranch = (branch: ASTNode): boolean => { - if (isNodeType("ConditionalExpression")(branch)) { - return checkFunctionBody(propertyChecker)(branch); - } - if (isNodeType("NewExpression")(branch)) { - return (branch as { arguments: unknown[] }).arguments - .filter(isNodeType("ObjectExpression")) - .some(checkObjectExpression(propertyChecker)); - } - return false; - }; - return ( - checkBranch((arg as { consequent: ASTNode }).consequent) && - checkBranch((arg as { alternate: ASTNode }).alternate) - ); - } - - if (isNodeType("NewExpression")(arg)) { - return (arg as { arguments: unknown[] }).arguments - .filter(isNodeType("ObjectExpression")) - .some(checkObjectExpression(propertyChecker)); - } - - return false; - } - - if ( - isNodeType("BlockStatement")(node) && - Array.isArray((node as { body: unknown[] }).body) - ) { - return (node as { body: ASTNode[] }).body.some( - checkFunctionBody(propertyChecker), - ); - } - - if (isNodeType("NewExpression")(node)) { - return (node as { arguments: unknown[] }).arguments - .filter(isNodeType("ObjectExpression")) - .some(checkObjectExpression(propertyChecker)); - } - - if (isNodeType("ConditionalExpression")(node)) { - const checkBranch = (branch: ASTNode): boolean => { - if (isNodeType("ConditionalExpression")(branch)) { - return checkFunctionBody(propertyChecker)(branch); - } - if (isNodeType("NewExpression")(branch)) { - return (branch as { arguments: unknown[] }).arguments - .filter(isNodeType("ObjectExpression")) - .some(checkObjectExpression(propertyChecker)); - } - return false; - }; - return ( - checkBranch((node as { consequent: ASTNode }).consequent) && - checkBranch((node as { alternate: ASTNode }).alternate) - ); - } - - return false; - }; - -/** - * Extracts parameter names from a function. - */ -const extractParams = ( - fn: FunctionNode, -): [string | null, string | null] => { - const params = (fn as { params: ASTNode[] }).params; - if (params.length < 2) return [null, null]; - - return params.slice(0, 2).map((node) => - isNodeType("Identifier")(node) ? (node as { name: string }).name : null - ) as [string | null, string | null]; -}; - -// ============================================================================ -// Dispatcher Tracker -// ============================================================================ - -/** - * Tracks dispatcher method calls on federation objects. - */ -const createDispatcherTracker = ( - dispatcherMethod: string, - federationTracker: ReturnType, -) => { - let dispatcherConfigured = false; - - const isDispatcherMethodCall = (node: CallExpression): boolean => - allOf( - hasMemberExpressionCallee, - hasIdentifierProperty, - hasMethodName(dispatcherMethod), - )( - node as unknown as { - callee: { property: { name: string }; object: ASTNode }; - } & CallExpression, - ); - - return { - isDispatcherConfigured: () => dispatcherConfigured, - checkDispatcherCall: (node: CallExpression) => { - if ( - isDispatcherMethodCall(node) && - federationTracker.isFederationObject( - (node.callee as { object: ASTNode }).object, - ) - ) { - dispatcherConfigured = true; - } - }, - }; -}; - -// ============================================================================ -// ESLint Rule Factory: Required Rules -// ============================================================================ - -/** - * Creates an ESLint rule that checks if a property is required. - */ -export function createESLintRequiredRule( - ruleId: string, - config: PropertyConfig, -): TSESLint.RuleModule { - const propertyChecker = config.path.length === 1 - ? createPropertyChecker(config.path[0]) - : createNestedPropertyChecker(config.path); - const propertySearcher = createPropertySearcher(propertyChecker); - - return { - meta: { - type: "suggestion", - docs: { - description: `Ensure actor dispatcher returns ${ - config.path.join(".") - } property`, - }, - schema: [], - messages: { - required: "{{ message }}", - }, - }, - defaultOptions: [], - create(context: ESLintRuleContext) { - const federationTracker = trackFederationVariables(); - const dispatcherTracker = createDispatcherTracker( - config.setter, - federationTracker, - ); - const actorDispatchers: ActorDispatcherInfo[] = []; - - return { - VariableDeclarator(node: TSESTree.VariableDeclarator) { - federationTracker.VariableDeclarator( - node as unknown as ASTNode & { - id: ASTNode; - init: ASTNode | null; - }, - ); - }, - - CallExpression(node: TSESTree.CallExpression) { - const callNode = node as unknown as CallExpression; - dispatcherTracker.checkDispatcherCall(callNode); - - if (!isSetActorDispatcherCall(callNode)) return; - if ( - !federationTracker.isFederationObject( - (callNode.callee as { object: ASTNode }).object, - ) - ) return; - - const dispatcherArg = callNode.arguments[1] as unknown as ASTNode; - if (isFunction(dispatcherArg)) { - actorDispatchers.push({ - node: callNode, - dispatcherArg: dispatcherArg as FunctionNode, - }); - } - }, - - "Program:exit"() { - if (!dispatcherTracker.isDispatcherConfigured()) return; - - for (const { dispatcherArg } of actorDispatchers) { - const body = (dispatcherArg as { body: ASTNode }).body; - if (!propertySearcher(body)) { - context.report({ - node: dispatcherArg as unknown as TSESTree.Node, - messageId: "required", - data: { message: actorPropertyRequired(config) }, - }); - } - } - }, - }; - }, - }; -} - -// ============================================================================ -// ESLint Rule Factory: Mismatch Rules -// ============================================================================ - -/** - * Creates an ESLint rule that checks if a property uses the correct context method. - */ -export function createESLintMismatchRule( - ruleId: string, - config: PropertyConfig, -): TSESLint.RuleModule { - return { - meta: { - type: "problem", - docs: { - description: `Ensure actor's ${ - config.path.join(".") - } property uses correct context method`, - }, - schema: [], - messages: { - mismatch: "{{ message }}", - }, - }, - defaultOptions: [], - create(context: ESLintRuleContext) { - const tracker = trackFederationVariables(); - - return { - VariableDeclarator(node: TSESTree.VariableDeclarator) { - tracker.VariableDeclarator( - node as unknown as ASTNode & { - id: ASTNode; - init: ASTNode | null; - }, - ); - }, - - CallExpression(node: TSESTree.CallExpression) { - const callNode = node as unknown as CallExpression; - - if ( - !isSetActorDispatcherCall(callNode) || - !hasMemberExpressionCallee(callNode) || - !tracker.isFederationObject( - (callNode.callee as { object: ASTNode }).object, - ) - ) return; - - const dispatcherArg = callNode.arguments[1] as unknown as ASTNode; - if (!isFunction(dispatcherArg)) return; - - const [ctxName, idName] = extractParams( - dispatcherArg as FunctionNode, - ); - if (!ctxName || !idName) return; - - const methodCallContext: MethodCallContext = { - path: config.path.join("."), - ctxName, - idName, - methodName: config.getter, - requiresIdentifier: config.requiresIdentifier, - }; - - const body = (dispatcherArg as { body: ASTNode }).body; - const existenceChecker = createPropertyExistenceChecker(config.path); - const hasProperty = checkFunctionBody(existenceChecker)(body); - - if (!hasProperty) return; - - const valueChecker = createPropertyValueChecker( - config.path, - methodCallContext, - ); - const hasCorrectValue = checkFunctionBody(valueChecker)(body); - - if (!hasCorrectValue) { - context.report({ - node: dispatcherArg as unknown as TSESTree.Node, - messageId: "mismatch", - data: { message: actorPropertyMismatch(methodCallContext) }, - }); - } - }, - }; - }, - }; -} - -// ============================================================================ -// ESLint Rule Factory: Collection Filtering -// ============================================================================ - -const FOLLOWERS_DISPATCHER_METHOD = "setFollowersDispatcher" as const; - -/** - * Checks if a node is a setFollowersDispatcher call. - */ -const isFollowersDispatcherCall = (node: CallExpression): boolean => - allOf( - hasMemberExpressionCallee, - hasIdentifierProperty, - hasMinArguments(2), - hasMethodName(FOLLOWERS_DISPATCHER_METHOD), - )( - node as unknown as { - callee: { property: { name: string }; object: ASTNode }; - } & CallExpression, - ); - -/** - * Checks if a function node has the filter parameter (4th parameter). - */ -const hasFilterParameter = (fn: FunctionNode): boolean => - (fn as { params: unknown[] }).params.length >= 4; - -/** - * Creates the collection-filtering-not-implemented ESLint rule. - */ -export function createESLintCollectionFilteringRule( - ruleId: string, -): TSESLint.RuleModule { - return { - meta: { - type: "suggestion", - docs: { - description: "Ensure followers dispatcher implements filtering", - }, - schema: [], - messages: { - filterRequired: COLLECTION_FILTERING_NOT_IMPLEMENTED_ERROR, - }, - }, - defaultOptions: [], - create(context: ESLintRuleContext) { - const federationTracker = trackFederationVariables(); - - return { - VariableDeclarator(node: TSESTree.VariableDeclarator) { - federationTracker.VariableDeclarator( - node as unknown as ASTNode & { - id: ASTNode; - init: ASTNode | null; - }, - ); - }, - - CallExpression(node: TSESTree.CallExpression) { - const callNode = node as unknown as CallExpression; - - if (!isFollowersDispatcherCall(callNode)) return; - if ( - !federationTracker.isFederationObject( - (callNode.callee as { object: ASTNode }).object, - ) - ) return; - - const dispatcherArg = callNode.arguments[1] as unknown as ASTNode; - if (!isFunction(dispatcherArg)) return; - - if (!hasFilterParameter(dispatcherArg as FunctionNode)) { - context.report({ - node: dispatcherArg as unknown as TSESTree.Node, - messageId: "filterRequired", - }); - } - }, - }; - }, - }; -} diff --git a/packages/lint/src/lib/eslint-types.ts b/packages/lint/src/lib/eslint-types.ts deleted file mode 100644 index eab4c7961..000000000 --- a/packages/lint/src/lib/eslint-types.ts +++ /dev/null @@ -1,141 +0,0 @@ -/** - * ESLint adapter types and utilities. - * Provides compatibility layer between ESLint and our common AST types. - */ -import type { TSESLint, TSESTree } from "@typescript-eslint/utils"; -import type { - ASTNode, - CallExpression, - FunctionNode, - VariableDeclarator, -} from "./ast-types.ts"; -import type { PropertyConfig } from "./types.ts"; - -// ============================================================================ -// ESLint Rule Types -// ============================================================================ - -/** - * ESLint rule context type - */ -export type ESLintRuleContext = TSESLint.RuleContext; - -/** - * ESLint rule module type - */ -export type ESLintRuleModule = TSESLint.RuleModule; - -/** - * ESLint rule listener type - */ -export type ESLintRuleListener = TSESLint.RuleListener; - -/** - * ESLint fixer type - */ -export type ESLintFixer = TSESLint.RuleFixer; - -// ============================================================================ -// Report Descriptor Types -// ============================================================================ - -export interface ReportDescriptor { - node: ASTNode; - message: string; -} - -// ============================================================================ -// Unified Rule Context Interface -// ============================================================================ - -/** - * Unified context interface for both Deno.lint and ESLint. - */ -export interface UnifiedRuleContext { - report(descriptor: ReportDescriptor): void; -} - -/** - * Creates a unified context from an ESLint rule context. - */ -export const createUnifiedContext = ( - eslintContext: ESLintRuleContext, - messageId: string, -): UnifiedRuleContext => ({ - report: ({ node, message }) => { - eslintContext.report({ - node: node as unknown as TSESTree.Node, - messageId, - data: { message }, - }); - }, -}); - -// ============================================================================ -// Unified Rule Visitor Interface -// ============================================================================ - -export interface UnifiedRuleVisitor { - VariableDeclarator?(node: VariableDeclarator): void; - CallExpression?(node: CallExpression): void; - "Program:exit"?(): void; -} - -// ============================================================================ -// Rule Factory Types -// ============================================================================ - -/** - * Actor dispatcher info for tracking - */ -export interface ActorDispatcherInfo { - node: CallExpression; - dispatcherArg: FunctionNode; -} - -/** - * Factory function type for creating rules - */ -export type RuleFactory = (config: PropertyConfig) => { - create(context: UnifiedRuleContext): UnifiedRuleVisitor; -}; - -// ============================================================================ -// ESLint Plugin Types -// ============================================================================ - -/** - * ESLint plugin configuration - */ -export interface ESLintPlugin { - meta: { - name: string; - version: string; - }; - rules: Record; - configs: Record; -} - -/** - * ESLint plugin config - */ -export interface ESLintPluginConfig { - plugins?: string[]; - rules?: Record; -} - -// ============================================================================ -// Node Type Guards (ESLint-specific) -// ============================================================================ - -/** - * Converts TSESTree node to common ASTNode type. - */ -export const toASTNode = (node: TSESTree.Node): ASTNode => - node as unknown as ASTNode; - -/** - * Converts common ASTNode to TSESTree node type. - */ -export const toTSESTreeNode = (node: ASTNode): TSESTree.Node => - node as unknown as TSESTree.Node; diff --git a/packages/lint/src/lib/property-checker-eslint.ts b/packages/lint/src/lib/property-checker-eslint.ts deleted file mode 100644 index 57cd1e47d..000000000 --- a/packages/lint/src/lib/property-checker-eslint.ts +++ /dev/null @@ -1,283 +0,0 @@ -/** - * Property checkers for ESLint rules. - * Uses TSESTree types from @typescript-eslint/utils. - */ -import { pipe, some } from "@fxts/core"; -import type { TSESTree } from "@typescript-eslint/utils"; -import type { MethodCallContext } from "./types.ts"; - -// ============================================================================ -// Type Aliases -// ============================================================================ - -type Node = TSESTree.Node; -type Property = TSESTree.Property; -type ObjectExpression = TSESTree.ObjectExpression; -type NewExpression = TSESTree.NewExpression; -type BlockStatement = TSESTree.BlockStatement; -type ReturnStatement = TSESTree.ReturnStatement; -type ConditionalExpression = TSESTree.ConditionalExpression; -type CallExpression = TSESTree.CallExpression; -type MemberExpression = TSESTree.MemberExpression; -type Identifier = TSESTree.Identifier; -type FunctionNode = - | TSESTree.ArrowFunctionExpression - | TSESTree.FunctionExpression; - -// ============================================================================ -// Basic Type Guards -// ============================================================================ - -const isProperty = (node: Node): node is Property => node.type === "Property"; - -const isObjectExpression = (node: Node): node is ObjectExpression => - node.type === "ObjectExpression"; - -const isNewExpression = (node: Node): node is NewExpression => - node.type === "NewExpression"; - -const isBlockStatement = (node: Node): node is BlockStatement => - node.type === "BlockStatement"; - -const isReturnStatement = (node: Node): node is ReturnStatement => - node.type === "ReturnStatement"; - -const isConditionalExpression = (node: Node): node is ConditionalExpression => - node.type === "ConditionalExpression"; - -const isCallExpression = (node: Node): node is CallExpression => - node.type === "CallExpression"; - -const isIdentifier = (node: Node): node is Identifier => - node.type === "Identifier"; - -const isMemberExpression = (node: Node): node is MemberExpression => - node.type === "MemberExpression"; - -// ============================================================================ -// Property Name Checkers -// ============================================================================ - -/** - * Checks if a Property node has an Identifier key with a specific name. - */ -const hasPropertyKeyName = (name: string) => (node: Property): boolean => - node.key.type === "Identifier" && node.key.name === name; - -/** - * Finds a property with a specific name in an ObjectExpression. - */ -const findPropertyByName = - (name: string) => (obj: ObjectExpression): Property | undefined => - obj.properties.find( - (p): p is Property => isProperty(p) && hasPropertyKeyName(name)(p), - ); - -// ============================================================================ -// ObjectExpression Extractors -// ============================================================================ - -/** - * Extracts the first ObjectExpression argument from a NewExpression. - */ -const extractObjectFromNewExpression = ( - node: NewExpression, -): ObjectExpression | null => { - const firstArg = node.arguments[0]; - if (firstArg && isObjectExpression(firstArg)) { - return firstArg; - } - return null; -}; - -/** - * Extracts ObjectExpression from a Property value. - * Handles both direct ObjectExpression and NewExpression with ObjectExpression argument. - */ -const extractObjectFromPropertyValue = ( - prop: Property, -): ObjectExpression | null => { - const value = prop.value; - if (isObjectExpression(value)) { - return value; - } - if (isNewExpression(value)) { - return extractObjectFromNewExpression(value); - } - return null; -}; - -// ============================================================================ -// Property Existence Checker -// ============================================================================ - -/** - * Recursively checks if a property path exists in an ObjectExpression. - */ -const checkPropertyPathExists = - (path: readonly string[]) => (obj: ObjectExpression): boolean => { - if (path.length === 0) return true; - - const prop = findPropertyByName(path[0])(obj); - if (!prop) return false; - - if (path.length === 1) return true; - - const nestedObj = extractObjectFromPropertyValue(prop); - if (!nestedObj) return false; - - return checkPropertyPathExists(path.slice(1))(nestedObj); - }; - -/** - * Creates a checker that verifies a property path exists. - */ -export const createPropertyExistenceChecker = - (path: readonly string[]) => (obj: ObjectExpression): boolean => - checkPropertyPathExists(path)(obj); - -// ============================================================================ -// Property Value Checker -// ============================================================================ - -/** - * Checks if a node is the expected method call. - * e.g., ctx.getActorUri(identifier) - */ -const isExpectedMethodCall = ( - node: Node, - ctx: MethodCallContext, -): boolean => { - if (!isCallExpression(node)) return false; - if (!isMemberExpression(node.callee)) return false; - - const { object, property } = node.callee; - if (!isIdentifier(object) || object.name !== ctx.ctxName) return false; - if (!isIdentifier(property) || property.name !== ctx.methodName) return false; - - if (!ctx.requiresIdentifier) { - return node.arguments.length === 0; - } - - return node.arguments.some( - (arg) => isIdentifier(arg) && arg.name === ctx.idName, - ); -}; - -/** - * Recursively checks if a property path has the correct method call value. - */ -const checkPropertyPathValue = - (path: readonly string[], ctx: MethodCallContext) => - (obj: ObjectExpression): boolean => { - if (path.length === 0) return false; - - const prop = findPropertyByName(path[0])(obj); - if (!prop) return false; - - if (path.length === 1) { - return isExpectedMethodCall(prop.value, ctx); - } - - const nestedObj = extractObjectFromPropertyValue(prop); - if (!nestedObj) return false; - - return checkPropertyPathValue(path.slice(1), ctx)(nestedObj); - }; - -/** - * Creates a checker that verifies a property has the correct method call value. - */ -export const createPropertyValueChecker = - (path: readonly string[], ctx: MethodCallContext) => - (obj: ObjectExpression): boolean => checkPropertyPathValue(path, ctx)(obj); - -// ============================================================================ -// Function Body Checkers -// ============================================================================ - -/** - * Extracts ObjectExpression from a node that might be a NewExpression. - */ -const extractObjectFromNode = (node: Node): ObjectExpression | null => { - if (isNewExpression(node)) { - return extractObjectFromNewExpression(node); - } - return null; -}; - -/** - * Checks a conditional expression branch for a property. - */ -const checkConditionalBranch = - (checker: (obj: ObjectExpression) => boolean) => (node: Node): boolean => { - if (isConditionalExpression(node)) { - return ( - checkConditionalBranch(checker)(node.consequent) && - checkConditionalBranch(checker)(node.alternate) - ); - } - - const obj = extractObjectFromNode(node); - return obj ? checker(obj) : false; - }; - -/** - * Searches a function body for ObjectExpressions and applies a checker. - */ -export const searchFunctionBody = - (checker: (obj: ObjectExpression) => boolean) => (body: Node): boolean => { - if (isBlockStatement(body)) { - return body.body.some(searchFunctionBody(checker)); - } - - if (isReturnStatement(body)) { - const arg = body.argument; - if (!arg) return false; - - if (isConditionalExpression(arg)) { - return checkConditionalBranch(checker)(arg); - } - - const obj = extractObjectFromNode(arg); - return obj ? checker(obj) : false; - } - - // Arrow function with direct expression body - if (isNewExpression(body)) { - const obj = extractObjectFromNewExpression(body); - return obj ? checker(obj) : false; - } - - if (isConditionalExpression(body)) { - return checkConditionalBranch(checker)(body); - } - - return false; - }; - -// ============================================================================ -// Parameter Extraction -// ============================================================================ - -/** - * Extracts the first two parameter names from a function. - * Returns [ctxName, idName] or [null, null] if not enough parameters. - */ -export const extractFunctionParams = ( - fn: FunctionNode, -): [string | null, string | null] => { - const params = fn.params; - if (params.length < 2) return [null, null]; - - const getName = (param: TSESTree.Parameter): string | null => - param.type === "Identifier" ? param.name : null; - - return [getName(params[0]), getName(params[1])]; -}; - -/** - * Checks if a function has at least n parameters. - */ -export const hasMinParams = (min: number) => (fn: FunctionNode): boolean => - fn.params.length >= min; diff --git a/packages/lint/src/lib/test-eslint.ts b/packages/lint/src/lib/test-eslint.ts deleted file mode 100644 index c6d406f7b..000000000 --- a/packages/lint/src/lib/test-eslint.ts +++ /dev/null @@ -1,89 +0,0 @@ -/** - * ESLint test utilities. - * Uses @typescript-eslint/rule-tester for testing ESLint rules. - */ -import { RuleTester } from "@typescript-eslint/rule-tester"; -import type { TSESLint } from "@typescript-eslint/utils"; -import { FEDERATION_SETUP } from "./const.ts"; - -// Configure RuleTester to use node:test -RuleTester.afterAll = () => {}; -RuleTester.it = (title, fn) => fn(); -RuleTester.describe = (_, fn) => fn(); - -export type RuleModule = TSESLint.RuleModule; - -export interface ESLintTestCase { - /** Test case name */ - name: string; - /** Code to test */ - code: string; -} - -export interface ESLintInvalidTestCase extends ESLintTestCase { - /** Expected error message IDs */ - errors: Array<{ messageId: string }>; -} - -/** - * Creates a RuleTester instance for testing ESLint rules. - */ -export function createRuleTester(): RuleTester { - return new RuleTester(); -} - -/** - * Wraps code with federation setup for testing. - */ -export function wrapWithFederationSetup( - code: string, - federationSetup: string | false = FEDERATION_SETUP, -): string { - if (federationSetup === false) return code; - return `${federationSetup}\n\n${code}`; -} - -/** - * Creates valid test cases with federation setup. - */ -export function validCase( - name: string, - code: string, - federationSetup: string | false = FEDERATION_SETUP, -): ESLintTestCase { - return { - name, - code: wrapWithFederationSetup(code, federationSetup), - }; -} - -/** - * Creates invalid test cases with federation setup and expected errors. - */ -export function invalidCase( - name: string, - code: string, - messageId: string, - federationSetup: string | false = FEDERATION_SETUP, -): ESLintInvalidTestCase { - return { - name, - code: wrapWithFederationSetup(code, federationSetup), - errors: [{ messageId }], - }; -} - -/** - * Runs ESLint rule tests. - */ -export function runESLintTests( - ruleName: string, - rule: RuleModule, - tests: { - valid: ESLintTestCase[]; - invalid: ESLintInvalidTestCase[]; - }, -): void { - const tester = createRuleTester(); - tester.run(ruleName, rule, tests); -} diff --git a/packages/lint/src/lib/test-templates-eslint.ts b/packages/lint/src/lib/test-templates-eslint.ts deleted file mode 100644 index 34d44399c..000000000 --- a/packages/lint/src/lib/test-templates-eslint.ts +++ /dev/null @@ -1,382 +0,0 @@ -/** - * ESLint test templates for generating test cases. - * Reuses patterns from Deno lint test templates. - */ -import { properties, type PropertyConfig } from "./const.ts"; -import type { ESLintInvalidTestCase, ESLintTestCase } from "./test-eslint.ts"; -import { invalidCase, validCase } from "./test-eslint.ts"; - -// ============================================================================= -// Types -// ============================================================================= - -type PropertyKey = keyof typeof properties; - -// ============================================================================= -// Common Code Snippets -// ============================================================================= - -const createDispatcherCode = (content: string): string => ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - ${content} - }); -`; - -const createChainedDispatcherCode = ( - content: string, - dispatcherMethod: string, -): string => ` - federation - .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - ${content} - }) - .${dispatcherMethod}(async (ctx, identifier) => []); -`; - -const createSeparateDispatcherCode = ( - content: string, - dispatcherMethod: string, - isBefore: boolean, -): string => { - const dispatcher = - `federation.${dispatcherMethod}(async (ctx, identifier) => []);`; - const actor = createDispatcherCode(content); - return isBefore ? `${dispatcher}\n${actor}` : `${actor}\n${dispatcher}`; -}; - -// ============================================================================= -// Property Code Generation Utilities -// ============================================================================= - -const createMethodCall = ( - getter: string, - requiresIdentifier: boolean, - ctxName = "ctx", - idName = "identifier", -): string => { - return requiresIdentifier - ? `${ctxName}.${getter}(${idName})` - : `${ctxName}.${getter}()`; -}; - -const createPropertyAssignment = ( - prop: PropertyConfig, - ctxName = "ctx", - idName = "identifier", -): string => { - const methodCall = createMethodCall( - prop.getter, - prop.requiresIdentifier, - ctxName, - idName, - ); - - if (prop.path.length === 1) { - return `${prop.path[0]}: ${methodCall},`; - } - - // Handle nested properties like endpoints.sharedInbox - if (prop.path.length === 2 && prop.path[0] === "endpoints") { - return `endpoints: new Endpoints({ ${prop.path[1]}: ${methodCall} }),`; - } - - return `${prop.path[prop.path.length - 1]}: ${methodCall},`; -}; - -const createReturnStatement = (properties: string): string => - `return new Person({ ${properties} });`; - -// ============================================================================= -// Required Rule Test Cases -// ============================================================================= - -export function createRequiredValidCases( - propKey: PropertyKey, -): ESLintTestCase[] { - const prop = properties[propKey]; - const propertyCode = createPropertyAssignment(prop); - - return [ - // Non-federation object - validCase( - "setActorDispatcher called on non-Federation object", - ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ name: "John Doe" }); - }); - `, - `const federation = { setActorDispatcher: () => {} };`, - ), - - // With property using method - validCase( - `with ${prop.path.join(".")} property using ctx.${prop.getter}()`, - createDispatcherCode( - createReturnStatement(`name: "John Doe", ${propertyCode}`), - ), - ), - - // With property using hardcoded value (for required rules, any value is fine) - validCase( - `with ${prop.path.join(".")} property (any value)`, - createDispatcherCode( - createReturnStatement( - `name: "John Doe", ${prop.path[prop.path.length - 1]}: "https://example.com/value"`, - ), - ), - ), - - // BlockStatement with property - validCase( - `BlockStatement with ${prop.path.join(".")}`, - createDispatcherCode(` - const name = "John Doe"; - ${createReturnStatement(`name, ${propertyCode}`)} - `), - ), - ]; -} - -export function createRequiredInvalidCases( - propKey: PropertyKey, - messageId: string, -): ESLintInvalidTestCase[] { - const prop = properties[propKey]; - - // For properties with setter !== setActorDispatcher, we need to add the - // dispatcher call to trigger the rule - if (prop.setter !== "setActorDispatcher") { - const dispatcherMethod = - prop.setter === "setInboxListeners" ? "setInboxListeners" : prop.setter; - - return [ - // Chained dispatcher case - this triggers the rule - invalidCase( - `chained ${dispatcherMethod} without ${prop.path.join(".")}`, - createChainedDispatcherCode( - createReturnStatement(`name: "John Doe"`), - dispatcherMethod, - ), - messageId, - ), - - // Separate dispatcher before - invalidCase( - `separate ${dispatcherMethod} before without ${prop.path.join(".")}`, - createSeparateDispatcherCode( - createReturnStatement(`name: "John Doe"`), - dispatcherMethod, - true, - ), - messageId, - ), - - // Separate dispatcher after - invalidCase( - `separate ${dispatcherMethod} after without ${prop.path.join(".")}`, - createSeparateDispatcherCode( - createReturnStatement(`name: "John Doe"`), - dispatcherMethod, - false, - ), - messageId, - ), - ]; - } - - // For id property (setter === setActorDispatcher), use basic cases - return [ - // Without property - invalidCase( - `without ${prop.path.join(".")} property`, - createDispatcherCode(createReturnStatement(`name: "John Doe"`)), - messageId, - ), - - // Empty object - invalidCase( - "returning empty object", - createDispatcherCode(createReturnStatement("")), - messageId, - ), - ]; -} - -// ============================================================================= -// Mismatch Rule Test Cases -// ============================================================================= - -export function createMismatchValidCases( - propKey: PropertyKey, -): ESLintTestCase[] { - const prop = properties[propKey]; - const propertyCode = createPropertyAssignment(prop); - - // Helper to create wrong property assignment for valid cases - const createWrongPropertyCode = (value: string): string => { - if (prop.path.length === 1) { - return `${prop.path[0]}: ${value}`; - } - // Handle nested properties like endpoints.sharedInbox - return `endpoints: new Endpoints({ ${prop.path[1]}: ${value} })`; - }; - - return [ - // Non-federation object - validCase( - "setActorDispatcher called on non-Federation object", - createDispatcherCode( - createReturnStatement( - `name: "John Doe", ${createWrongPropertyCode('"https://example.com/wrong"')}`, - ), - ), - `const federation = { setActorDispatcher: () => {} };`, - ), - - // Correct method call - validCase( - `${prop.path.join(".")} uses ctx.${prop.getter}()`, - createDispatcherCode( - createReturnStatement(`name: "John Doe", ${propertyCode}`), - ), - ), - - // BlockStatement with correct method - validCase( - `BlockStatement with correct ${prop.path.join(".")}`, - createDispatcherCode(` - const name = "John Doe"; - ${createReturnStatement(`name, ${propertyCode}`)} - `), - ), - - // No property at all (let required rule handle this) - validCase( - `no ${prop.path.join(".")} property (required rule handles this)`, - createDispatcherCode(createReturnStatement(`name: "John Doe"`)), - ), - ]; -} - -export function createMismatchInvalidCases( - propKey: PropertyKey, - messageId: string, -): ESLintInvalidTestCase[] { - const prop = properties[propKey]; - - // Helper to create wrong property assignment - const createWrongPropertyAssignment = (value: string): string => { - if (prop.path.length === 1) { - return `${prop.path[0]}: ${value}`; - } - // Handle nested properties like endpoints.sharedInbox - return `endpoints: new Endpoints({ ${prop.path[1]}: ${value} })`; - }; - - return [ - // Hardcoded string - invalidCase( - `${prop.path.join(".")} uses hardcoded string`, - createDispatcherCode( - createReturnStatement( - `name: "John Doe", ${createWrongPropertyAssignment('"https://example.com/wrong"')}`, - ), - ), - messageId, - ), - - // Wrong method - invalidCase( - `${prop.path.join(".")} uses wrong method`, - createDispatcherCode( - createReturnStatement( - `name: "John Doe", ${createWrongPropertyAssignment("ctx.getWrongMethod()")}`, - ), - ), - messageId, - ), - ]; -} - -// ============================================================================= -// Collection Filtering Test Cases -// ============================================================================= - -const createFollowersDispatcherCode = ( - { - params = ["ctx", "identifier", "cursor", "filter"], - async: isAsync = true, - arrow = true, - }: { - params?: readonly string[]; - async?: boolean; - arrow?: boolean; - } = {}, -): string => { - const paramsString = params.join(", "); - const asyncKeyword = isAsync ? "async" : ""; - const [funcKeyword, arrowSymbol] = arrow ? ["", "=>"] : ["function", ""]; - - return ` - federation.setFollowersDispatcher( - "/users/{identifier}/followers", - ${asyncKeyword} ${funcKeyword}(${paramsString}) ${arrowSymbol} { - return { items: [] }; - } - ); - `; -}; - -const filterless = ["ctx", "identifier", "cursor"] as const; - -export function createCollectionFilteringValidCases(): ESLintTestCase[] { - return [ - validCase( - "async arrow function with filter parameter", - createFollowersDispatcherCode(), - ), - validCase( - "async function expression with filter", - createFollowersDispatcherCode({ arrow: false }), - ), - validCase( - "sync arrow function with filter", - createFollowersDispatcherCode({ async: false }), - ), - validCase( - "sync function expression with filter", - createFollowersDispatcherCode({ async: false, arrow: false }), - ), - ]; -} - -export function createCollectionFilteringInvalidCases( - messageId: string, -): ESLintInvalidTestCase[] { - return [ - invalidCase( - "async arrow function without filter parameter", - createFollowersDispatcherCode({ params: filterless }), - messageId, - ), - invalidCase( - "async function expression without filter", - createFollowersDispatcherCode({ params: filterless, arrow: false }), - messageId, - ), - invalidCase( - "sync arrow function without filter", - createFollowersDispatcherCode({ params: filterless, async: false }), - messageId, - ), - invalidCase( - "sync function expression without filter", - createFollowersDispatcherCode({ - params: filterless, - async: false, - arrow: false, - }), - messageId, - ), - ]; -} From 62a4ec9d8407daf5f685deda56094b6cc571bfc7 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Mon, 15 Dec 2025 08:32:21 +0000 Subject: [PATCH 27/41] Replace `@typescript-eslint` with `estree` except `@typescript-eslint/parser` --- packages/lint/deno.json | 6 +- packages/lint/package.json | 6 +- packages/lint/src/index.ts | 47 ++--- .../lint/src/lib/mismatch-rule-factory.ts | 27 ++- packages/lint/src/lib/pred.ts | 12 +- packages/lint/src/lib/property-checker.ts | 43 ++-- .../lint/src/lib/required-rule-factory.ts | 18 +- packages/lint/src/lib/test-templates.ts | 4 +- packages/lint/src/lib/test.ts | 79 ++++---- packages/lint/src/lib/tracker.ts | 14 +- packages/lint/src/lib/types.ts | 58 +++--- .../collection-filtering-not-implemented.ts | 8 +- packages/lint/src/tests/integration.test.ts | 183 +++++++++++++----- 13 files changed, 299 insertions(+), 206 deletions(-) diff --git a/packages/lint/deno.json b/packages/lint/deno.json index 6f2901518..5c31da9d9 100644 --- a/packages/lint/deno.json +++ b/packages/lint/deno.json @@ -6,10 +6,8 @@ ".": "./src/mod.ts" }, "imports": { - "@types/eslint": "npm:@types/eslint@^9.0.0", - "@typescript-eslint/rule-tester": "npm:@typescript-eslint/rule-tester@^8.49.0", - "@typescript-eslint/utils": "npm:@typescript-eslint/utils@^8.49.0", - "eslint": "npm:eslint@^9.0.0" + "eslint": "npm:eslint@^9.0.0", + "estree": "npm:@types/estree@^1.0.8" }, "tasks": { "test": "deno test --allow-all" diff --git a/packages/lint/package.json b/packages/lint/package.json index fd5cdf558..517b52e14 100644 --- a/packages/lint/package.json +++ b/packages/lint/package.json @@ -59,7 +59,9 @@ }, "devDependencies": { "@types/eslint": "^9.0.0", - "@typescript-eslint/rule-tester": "^8.49.0", + "@types/estree": "^1.0.8", + "@typescript-eslint/parser": "^8.49.0", + "eslint": "catalog:", "tsdown": "catalog:", "typescript": "catalog:" }, @@ -67,6 +69,6 @@ "build": "tsdown", "prepack": "tsdown", "prepublish": "tsdown", - "test": "node --experimental-transform-types" + "test": "node --experimental-transform-types --test 'src/tests/**/*.test.ts'" } } diff --git a/packages/lint/src/index.ts b/packages/lint/src/index.ts index d07863474..dc3358890 100644 --- a/packages/lint/src/index.ts +++ b/packages/lint/src/index.ts @@ -2,8 +2,8 @@ * ESLint plugin for Fedify. * Provides lint rules for validating Fedify federation code. */ -import { keys, pipe, reduceLazy } from "@fxts/core"; -import type { TSESLint } from "@typescript-eslint/utils"; +import { fromEntries, keys, map, pipe } from "@fxts/core"; +import type { ESLint, Rule } from "eslint"; import metadata from "../deno.json" with { type: "json" }; import { RULE_IDS } from "./lib/const.ts"; import { @@ -68,7 +68,7 @@ import { const rules: Record< typeof RULE_IDS[keyof typeof RULE_IDS], - TSESLint.RuleModule + Rule.RuleModule > = { [RULE_IDS.actorIdMismatch]: actorIdMismatch, [RULE_IDS.actorIdRequired]: actorIdRequired, @@ -95,43 +95,44 @@ const rules: Record< [RULE_IDS.collectionFilteringNotImplemented]: collectionFiltering, }; +const recommendedRuleIds: (keyof typeof rules)[] = [ + RULE_IDS.actorIdMismatch, + RULE_IDS.actorIdRequired, +]; + /** * Recommended configuration - enables all rules as warnings */ const recommendedRules = pipe( rules, keys, - reduceLazy((acc, key) => { - acc[`@fedify/lint/${key}`] = recommendedRuleIds - .includes(key) - ? "error" as const - : "warn" as const; - return acc; - }, {} as Record), + map((key) => + [ + `@fedify/lint/${key}`, + recommendedRuleIds + .includes(key) + ? "error" as const + : "warn" as const, + ] as const + ), + fromEntries, ); -const recommendedRuleIds: (keyof typeof rules)[] = [ - RULE_IDS.actorIdMismatch, - RULE_IDS.actorIdRequired, -]; - /** * Strict configuration - enables all rules as errors */ const strictRules = pipe( - Object.keys(rules), - (keys) => - keys.reduce((acc, key) => { - acc[`@fedify/lint/${key}`] = "error" as const; - return acc; - }, {} as Record), + rules, + keys, + map((key) => [`@fedify/lint/${key}`, "error" as const] as const), + fromEntries, ); // ============================================================================ // Plugin Export // ============================================================================ -const plugin: TSESLint.Linter.Plugin = { +const plugin = { meta: { name: metadata.name, version: metadata.version, @@ -147,6 +148,6 @@ const plugin: TSESLint.Linter.Plugin = { rules: strictRules, }, }, -}; +} as const satisfies ESLint.Plugin; export default plugin; diff --git a/packages/lint/src/lib/mismatch-rule-factory.ts b/packages/lint/src/lib/mismatch-rule-factory.ts index 7e0e4409b..da36d50ad 100644 --- a/packages/lint/src/lib/mismatch-rule-factory.ts +++ b/packages/lint/src/lib/mismatch-rule-factory.ts @@ -1,5 +1,5 @@ import { isEmpty, negate, some } from "@fxts/core"; -import type { TSESLint } from "@typescript-eslint/utils"; +import type { Rule } from "eslint"; import { actorPropertyMismatch } from "./messages.ts"; import { allOf, @@ -15,16 +15,15 @@ import { } from "./property-checker.ts"; import { trackFederationVariables } from "./tracker.ts"; import type { - AssignmentPattern, Expression, FunctionNode, Identifier, MethodCallContext, + Node, Parameter, PrivateIdentifier, PropertyConfig, SpreadElement, - TSEmptyBodyFunctionExpression, } from "./types.ts"; const isIdentifierWithName = (name: T) => @@ -44,12 +43,7 @@ const isExpectedMethodCall = ( requiresIdentifier, }: MethodCallContext, ) => -( - node: - | Expression - | AssignmentPattern - | TSEmptyBodyFunctionExpression, -): boolean => { +(node: Node): boolean => { if ( !isNodeType("CallExpression")(node) || !hasMemberExpressionCallee(node) || @@ -80,9 +74,7 @@ const extractParams = ( }; const getNameIfIdentifier = (node: Parameter): string | null => - isNodeType("Identifier")(node as Identifier) - ? (node as Identifier).name - : null; + node?.type === "Identifier" ? node.name : null; /** * Creates a lint rule that checks if a property uses the correct context method. * @@ -150,7 +142,7 @@ export const createMismatchRuleDeno = ( export function createMismatchRuleEslint( { path, getter, requiresIdentifier = true }: PropertyConfig, -): TSESLint.RuleModule { +): Rule.RuleModule { return { meta: { type: "problem", @@ -164,7 +156,6 @@ export function createMismatchRuleEslint( mismatch: "{{ message }}", }, }, - defaultOptions: [], create(context) { const tracker = trackFederationVariables(); @@ -192,6 +183,14 @@ export function createMismatchRuleEslint( requiresIdentifier, }; + const existenceChecker = createPropertyChecker(Boolean); + const hasProperty = createPropertySearcher(existenceChecker(path))( + dispatcherArg.body, + ); + + // If property doesn't exist, don't report (that's for *-required rules) + if (!hasProperty) return; + // Property exists, now check if the value is correct const propertyChecker = createPropertyChecker( isExpectedMethodCall(methodCallContext), diff --git a/packages/lint/src/lib/pred.ts b/packages/lint/src/lib/pred.ts index 7e2d2d03b..f826a40c4 100644 --- a/packages/lint/src/lib/pred.ts +++ b/packages/lint/src/lib/pred.ts @@ -1,5 +1,4 @@ import { pipe, prop } from "@fxts/core"; -import type { TSESTree } from "@typescript-eslint/utils"; import type { CallExpression, CallMemberExpression, @@ -38,8 +37,7 @@ export const anyOf = * Checks if a node is of a specific type. */ export const isNodeType = - (type: T) => - (node: Node): node is { "type": T } & Node => + (type: T) => (node: Node): node is { "type": T } & Node => pipe( node, prop("type"), @@ -57,14 +55,6 @@ export const isNodeName = eq(name), ) as boolean; -/** - * Checks if a node's key is an Identifier. - */ -export const hasIdentifierKey = ( - node: T, -): node is T & { "key": Deno.lint.Identifier } => - pipe(node, prop("key"), isNodeType("Identifier")) as boolean; - /** * Checks if a node's callee is a MemberExpression. */ diff --git a/packages/lint/src/lib/property-checker.ts b/packages/lint/src/lib/property-checker.ts index da971e14e..8f6dc5e1a 100644 --- a/packages/lint/src/lib/property-checker.ts +++ b/packages/lint/src/lib/property-checker.ts @@ -1,26 +1,29 @@ import { + always, + head, isEmpty, isNil, isObject, pipe, pipeLazy, prop, - some, + unless, when, } from "@fxts/core"; import { allOf, isNodeType } from "./pred.ts"; import type { AssignmentPattern, BlockStatement, + ConditionalExpression, Expression, NewExpression, Node, + ObjectExpression, Property, PropertyChecker, ReturnStatement, SpreadElement, Statement, - TSEmptyBodyFunctionExpression, WithIdentifierKey, } from "./types.ts"; import { eq } from "./utils.ts"; @@ -61,8 +64,7 @@ export function createPropertyChecker( checker: ( node: | Expression - | AssignmentPattern - | TSEmptyBodyFunctionExpression, + | AssignmentPattern, ) => boolean, ): (path: readonly string[]) => PropertyChecker { const inner = @@ -70,7 +72,9 @@ export function createPropertyChecker( if (!isPropertyWithName(first)(node)) return false; // Base case: last property in path - if (isEmpty(rest)) return checker(node.value); + if (isEmpty(rest)) { + return checker(node.value as Expression | AssignmentPattern); + } // Handle NewExpression: endpoints: new Endpoints({ sharedInbox: ... }) if (isNodeType("NewExpression")(node.value)) { @@ -91,8 +95,7 @@ export function createPropertyChecker( * @returns A function that checks the ObjectExpression */ const checkObjectExpression = - (propertyChecker: PropertyChecker) => - (obj: Deno.lint.ObjectExpression): boolean => + (propertyChecker: PropertyChecker) => (obj: ObjectExpression): boolean => obj.properties.some(propertyChecker); /** @@ -103,7 +106,7 @@ const checkObjectExpression = const checkConditionalExpression = ( propertyChecker: PropertyChecker, ) => -(node: Deno.lint.ConditionalExpression): boolean => +(node: ConditionalExpression): boolean => [node.consequent, node.alternate].every(checkBranchWith(propertyChecker)); // Check if both branches have the property @@ -120,9 +123,7 @@ const checkBranchWith = /** * Extracts ObjectExpression from NewExpression. */ -const extractObjectExpression = ( - arg: Expression, -): Deno.lint.ObjectExpression | null => { +const extractObjectExpression = (arg: Expression): ObjectExpression | null => { if (isNodeType("NewExpression")(arg)) { return extractFirstObjectExpression(arg); } @@ -132,12 +133,18 @@ const extractObjectExpression = ( /** * Extracts the first argument if it's an ObjectExpression. */ -const extractFirstObjectExpression = (node: Deno.lint.NewExpression): - | Deno.lint.ObjectExpression - | null => { - const firstArg = node.arguments[0]; - return isNodeType("ObjectExpression")(firstArg) ? firstArg : null; -}; +const extractFirstObjectExpression = (node: NewExpression): + | ObjectExpression + | null => + pipe( + node, + prop("arguments"), + head, + unless( + isNodeType("ObjectExpression"), + always(null), + ) as () => ObjectExpression | null, + ); /** * Checks if a ReturnStatement node contains a property. @@ -147,7 +154,7 @@ const extractFirstObjectExpression = (node: Deno.lint.NewExpression): export const checkReturnStatement = ( propertyChecker: PropertyChecker, ) => -(node: Deno.lint.ReturnStatement) => { +(node: ReturnStatement) => { const arg = node.argument; if (isNil(arg)) return false; diff --git a/packages/lint/src/lib/required-rule-factory.ts b/packages/lint/src/lib/required-rule-factory.ts index 5dc91b2bb..1af56ccde 100644 --- a/packages/lint/src/lib/required-rule-factory.ts +++ b/packages/lint/src/lib/required-rule-factory.ts @@ -1,4 +1,5 @@ -import type { TSESLint, TSESTree } from "@typescript-eslint/utils"; +import type { Rule } from "eslint"; +import type * as ESTree from "estree"; import { actorPropertyRequired } from "./messages.ts"; import { allOf, @@ -65,8 +66,8 @@ interface ActorDispatcherInfoDeno { | Deno.lint.FunctionExpression; } -function createRequiredRule< - Context = Deno.lint.RuleContext | TSESLint.RuleContext, +function _createRequiredRule< + Context = Deno.lint.RuleContext | Rule.RuleContext, /* CallExpression = Context extends Deno.lint.RuleContext ? Deno.lint.CallExpression : TSESTree.CallExpression, */ @@ -183,10 +184,10 @@ export function createRequiredRuleDeno( } interface ActorDispatcherInfoEslint { - node: TSESTree.CallExpression; + node: ESTree.CallExpression; dispatcherArg: - | TSESTree.ArrowFunctionExpression - | TSESTree.FunctionExpression; + | ESTree.ArrowFunctionExpression + | ESTree.FunctionExpression; } /** @@ -195,7 +196,7 @@ interface ActorDispatcherInfoEslint { export function createRequiredRuleEslint( config: PropertyConfig, -): TSESLint.RuleModule { +): Rule.RuleModule { return { meta: { type: "suggestion", @@ -209,8 +210,7 @@ export function createRequiredRuleEslint( required: "{{ message }}", }, }, - defaultOptions: [], - create(context: TSESLint.RuleContext) { + create(context) { const federationTracker = trackFederationVariables(); const dispatcherTracker = createDispatcherTracker( config.setter, diff --git a/packages/lint/src/lib/test-templates.ts b/packages/lint/src/lib/test-templates.ts index b7a8f1c61..6968fcd15 100644 --- a/packages/lint/src/lib/test-templates.ts +++ b/packages/lint/src/lib/test-templates.ts @@ -1,4 +1,4 @@ -import type { TSESLint } from "@typescript-eslint/utils"; +import type { Rule } from "eslint"; import { properties } from "./const.ts"; import { actorPropertyMismatch, actorPropertyRequired } from "./messages.ts"; import lintTest from "./test.ts"; @@ -9,7 +9,7 @@ type PropertyKey = keyof typeof properties; interface TestConfig { rule: { deno: Deno.lint.Rule; - eslint: TSESLint.RuleModule; + eslint: Rule.RuleModule; }; ruleName: string; } diff --git a/packages/lint/src/lib/test.ts b/packages/lint/src/lib/test.ts index fb82ffc85..5bec4c805 100644 --- a/packages/lint/src/lib/test.ts +++ b/packages/lint/src/lib/test.ts @@ -1,6 +1,6 @@ import { isNil } from "@fxts/core"; -import { RuleTester } from "@typescript-eslint/rule-tester"; -import type { TSESLint } from "@typescript-eslint/utils"; +import * as tsParser from "@typescript-eslint/parser"; +import { Linter, type Rule } from "eslint"; import assert from "node:assert/strict"; import { FEDERATION_SETUP } from "./const.ts"; @@ -36,13 +36,7 @@ function testDenoLint( assert.equal( diagnostics.length, 0, - `Should not report issues when id property is present but found: \n${ - diagnostics.map((d) => " - " + d.message).join(", ") - } - -=== CODE === -${code} -=======`, + "Should not report issues when id property is present but found:", ); } else { assert.ok( @@ -60,10 +54,7 @@ ${code} } } -RuleTester.afterAll = () => {}; -RuleTester.describe = () => {}; - -function testEslintRule( +function testEslint( { code, rule, @@ -72,41 +63,57 @@ function testEslintRule( expectedError, }: { code: string; - rule: TSESLint.RuleModule; + rule: Rule.RuleModule; ruleName: string; federationSetup?: string; expectedError?: string; }, ) { - const ruleTester = new RuleTester({ + const linter = new Linter({ configType: "flat" }); + + const config = [{ + files: ["**/*.ts"], + plugins: { + "fedify-test": { + rules: { [ruleName]: rule }, + }, + }, + rules: { + [`fedify-test/${ruleName}`]: expectedError ? "error" : "off", + }, languageOptions: { + parser: tsParser, ecmaVersion: "latest", sourceType: "module", - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - }, }, - }); + } as Linter.Config]; const fullCode = `${federationSetup ?? federationSetup}\n\n${code}`; + const results = linter.verify(fullCode, config, `${ruleName}.test.ts`); if (isNil(expectedError)) { - ruleTester.run(ruleName, rule, { - valid: [fullCode], - invalid: [], - }); + assert.equal( + results.length, + 0, + `Should not report issues but found: ${ + results.map((r) => r.message).join(", ") + }`, + ); } else { - ruleTester.run(ruleName, rule, { - valid: [], - invalid: [ - { - code: fullCode, - errors: [{ messageId: expectedError }], - }, - ], - }); + assert.ok( + results.length > 0, + "Expected at least one diagnostic error but found none", + ); + const matched = results.some((r) => + r.message.includes(expectedError) || + r.ruleId === `fedify-test/${ruleName}` + ); + assert.ok( + matched, + `Expected fedify-test/${ruleName} to report but it did not. Got: ${ + results.map((r) => r.message).join(", ") + }`, + ); } } @@ -121,7 +128,7 @@ export default function lintTest( code: string; rule: { deno: Deno.lint.Rule; - eslint: TSESLint.RuleModule; + eslint: Rule.RuleModule; }; ruleName: string; federationSetup?: string; @@ -139,7 +146,7 @@ export default function lintTest( }); } else { return () => - testEslintRule({ + testEslint({ code, rule: eslint, ruleName, diff --git a/packages/lint/src/lib/tracker.ts b/packages/lint/src/lib/tracker.ts index 50cc74c7d..4a7b8293e 100644 --- a/packages/lint/src/lib/tracker.ts +++ b/packages/lint/src/lib/tracker.ts @@ -1,4 +1,8 @@ -import type { TSESTree } from "@typescript-eslint/utils"; +import type { + CallExpression, + Expression, + VariableDeclarator, +} from "./types.ts"; /** * Helper to track variable names that store the result of createFederation() or createFederationBuilder() calls @@ -7,7 +11,7 @@ export function trackFederationVariables() { const federationVariables = new Set(); const isFederationObject = ( - obj: Deno.lint.Expression | TSESTree.Expression, + obj: Expression, ): boolean => { switch (obj.type) { case "Identifier": @@ -27,9 +31,7 @@ export function trackFederationVariables() { }; return { - VariableDeclarator( - node: Deno.lint.VariableDeclarator | TSESTree.VariableDeclarator, - ): void { + VariableDeclarator(node: VariableDeclarator): void { const init = node.init; const id = node.id; @@ -52,7 +54,7 @@ export function trackFederationVariables() { } const isCreateFederation = ( - node: Deno.lint.CallExpression, + node: CallExpression, ): boolean => node.callee.type === "Identifier" && /^create(Federation|FederationBuilder)$/i.test(node.callee.name); diff --git a/packages/lint/src/lib/types.ts b/packages/lint/src/lib/types.ts index 4d3dd233a..1806f9241 100644 --- a/packages/lint/src/lib/types.ts +++ b/packages/lint/src/lib/types.ts @@ -1,40 +1,51 @@ -import type { TSESTree } from "@typescript-eslint/utils"; +import type * as ESTree from "estree"; -export type BlockStatement = Deno.lint.BlockStatement | TSESTree.BlockStatement; -export type CallExpression = Deno.lint.CallExpression | TSESTree.CallExpression; -export type Expression = Deno.lint.Expression | TSESTree.Expression; -export type Identifier = Deno.lint.Identifier | TSESTree.Identifier; +export type BlockStatement = + | Deno.lint.BlockStatement + | ESTree.BlockStatement; +export type CallExpression = Deno.lint.CallExpression | ESTree.CallExpression; +export type ConditionalExpression = + | Deno.lint.ConditionalExpression + | ESTree.ConditionalExpression; +export type Expression = + | Deno.lint.Expression + | ESTree.Expression + | ESTree.Super; +export type Identifier = Deno.lint.Identifier | ESTree.Identifier; export type MemberExpression = | Deno.lint.MemberExpression - | TSESTree.MemberExpression; -export type NewExpression = Deno.lint.NewExpression | TSESTree.NewExpression; -export type Node = Deno.lint.Node | TSESTree.Node; + | ESTree.MemberExpression; +export type NewExpression = Deno.lint.NewExpression | ESTree.NewExpression; +export type Node = Deno.lint.Node | ESTree.Node; export type ObjectExpression = | Deno.lint.ObjectExpression - | TSESTree.ObjectExpression; -export type Parameter = Deno.lint.Parameter | TSESTree.Parameter; + | ESTree.ObjectExpression; +export type ObjectPattern = + | Deno.lint.ObjectPattern + | ESTree.ObjectPattern; +export type Parameter = Deno.lint.Parameter | ESTree.Pattern; export type PrivateIdentifier = | Deno.lint.PrivateIdentifier - | TSESTree.PrivateIdentifier; -export type Property = Deno.lint.Property | TSESTree.Property; + | ESTree.PrivateIdentifier; +export type Property = Deno.lint.Property | ESTree.Property; export type ReturnStatement = | Deno.lint.ReturnStatement - | TSESTree.ReturnStatement; -export type SpreadElement = Deno.lint.SpreadElement | TSESTree.SpreadElement; -export type Statement = Deno.lint.Statement | TSESTree.Statement; + | ESTree.ReturnStatement; +export type SpreadElement = Deno.lint.SpreadElement | ESTree.SpreadElement; +export type Statement = Deno.lint.Statement | ESTree.Statement; +export type VariableDeclarator = + | Deno.lint.VariableDeclarator + | ESTree.VariableDeclarator; export type AssignmentPattern = | Deno.lint.AssignmentPattern - | TSESTree.AssignmentPattern; -export type TSEmptyBodyFunctionExpression = - | Deno.lint.TSEmptyBodyFunctionExpression - | TSESTree.TSEmptyBodyFunctionExpression; + | ESTree.AssignmentPattern; export type FunctionNode = | Deno.lint.ArrowFunctionExpression | Deno.lint.FunctionExpression - | TSESTree.ArrowFunctionExpression - | TSESTree.FunctionExpression; + | ESTree.ArrowFunctionExpression + | ESTree.FunctionExpression; export type CallMemberExpression = CallExpression & { callee: MemberExpression; @@ -102,8 +113,5 @@ export interface MethodCallContext { } export interface WithIdentifierKey { - key: { - type: "Identifier" | TSESTree.AST_NODE_TYPES.Identifier; - name: T; - }; + key: Identifier & { name: T }; } diff --git a/packages/lint/src/rules/collection-filtering-not-implemented.ts b/packages/lint/src/rules/collection-filtering-not-implemented.ts index 8b8b1d77e..77eaebf77 100644 --- a/packages/lint/src/rules/collection-filtering-not-implemented.ts +++ b/packages/lint/src/rules/collection-filtering-not-implemented.ts @@ -1,4 +1,4 @@ -import type { TSESLint } from "@typescript-eslint/utils"; +import type { Rule } from "eslint"; import { COLLECTION_FILTERING_NOT_IMPLEMENTED_ERROR, COLLECTION_FILTERING_NOT_IMPLEMENTED_ERROR as message, @@ -100,10 +100,7 @@ export const deno: Deno.lint.Rule = { }, }; -export const eslint: TSESLint.RuleModule< - string, - unknown[] -> = { +export const eslint: Rule.RuleModule = { meta: { type: "suggestion", docs: { @@ -114,7 +111,6 @@ export const eslint: TSESLint.RuleModule< filterRequired: COLLECTION_FILTERING_NOT_IMPLEMENTED_ERROR, }, }, - defaultOptions: [], create(context) { const federationTracker = trackFederationVariables(); diff --git a/packages/lint/src/tests/integration.test.ts b/packages/lint/src/tests/integration.test.ts index abf5dfc5b..a4f2ef12f 100644 --- a/packages/lint/src/tests/integration.test.ts +++ b/packages/lint/src/tests/integration.test.ts @@ -5,24 +5,67 @@ * All tests start from the complete valid code and modify only the part * necessary to trigger the specific lint rule being tested. */ + +import * as parser from "@typescript-eslint/parser"; +import { Linter } from "eslint"; import { equal, ok } from "node:assert/strict"; import { test } from "node:test"; -import plugin from "../mod.ts"; +import eslintPlugin from "../index.ts"; +import denoPlugin from "../mod.ts"; + +const PLUGIN_NAME = "Deno" in globalThis ? "fedify-lint" : "@fedify/lint"; -const PLUGIN_NAME = "fedify-lint"; +type Diagnostic = { + id: string; + message: string; +}; /** * Run all lint rules on the given code and return diagnostics. */ -function lintCode(code: string): Deno.lint.Diagnostic[] { - return Deno.lint.runPlugin(plugin, "integration.test.ts", code); +const lintTest = (code: string): Diagnostic[] => + "Deno" in globalThis ? testDenoLint(code) : testEslint(code); + +const testDenoLint = (code: string) => + Deno.lint.runPlugin( + denoPlugin, + "integration.test.ts", + code, + ) as Diagnostic[]; + +function testEslint(code: string) { + // For Node.js environment using ESLint flat config + const linter = new Linter({ configType: "flat" }); + + const config = [{ + files: ["**/*.ts"], + plugins: { + "@fedify/lint": { + meta: eslintPlugin.meta, + rules: eslintPlugin.rules, + }, + }, + rules: eslintPlugin.configs.recommended.rules, + languageOptions: { + ecmaVersion: 2022, + sourceType: "module", + parser, + }, + } as Linter.Config]; + + const results = linter.verify(code, config, "integration.test.ts"); + + return results.map((msg) => ({ + id: msg.ruleId ?? "unknown", + message: msg.message, + })); } /** * Assert that the code passes all lint rules (no diagnostics). */ function assertNoErrors(code: string, message?: string) { - const diagnostics = lintCode(code); + const diagnostics = lintTest(code); equal( diagnostics.length, 0, @@ -37,7 +80,7 @@ function assertNoErrors(code: string, message?: string) { * Assert that the code has exactly one error matching the given rule. */ function assertHasError(code: string, ruleName: string, message?: string) { - const diagnostics = lintCode(code); + const diagnostics = lintTest(code); const ruleId = `${PLUGIN_NAME}/${ruleName}`; const matched = diagnostics.some((d) => d.id === ruleId); ok( @@ -351,8 +394,11 @@ test("Integration: ✅ No setFollowingDispatcher - following property not requir // Remove the following property AND the setFollowingDispatcher const code = COMPLETE_VALID_CODE .replace( - "following: ctx.getFollowingUri(identifier),", - "// following: REMOVED", + ` outbox: ctx.getOutboxUri(identifier), + following: ctx.getFollowingUri(identifier), + followers: ctx.getFollowersUri(identifier),`, + ` outbox: ctx.getOutboxUri(identifier), + followers: ctx.getFollowersUri(identifier),`, ) .replace( `federation.setFollowingDispatcher( @@ -360,17 +406,23 @@ test("Integration: ✅ No setFollowingDispatcher - following property not requir async (_ctx, _identifier, _cursor) => { return { items: [] }; }, -);`, - "// setFollowingDispatcher REMOVED", +); + +`, + "", ); + assertNoErrors(code); }); test("Integration: ✅ No setFollowersDispatcher - followers property not required", () => { const code = COMPLETE_VALID_CODE .replace( - "followers: ctx.getFollowersUri(identifier),", - "// followers: REMOVED", + ` following: ctx.getFollowingUri(identifier), + followers: ctx.getFollowersUri(identifier), + liked: ctx.getLikedUri(identifier),`, + ` following: ctx.getFollowingUri(identifier), + liked: ctx.getLikedUri(identifier),`, ) .replace( `federation.setFollowersDispatcher( @@ -378,8 +430,10 @@ test("Integration: ✅ No setFollowersDispatcher - followers property not requir async (_ctx, _identifier, _cursor, _filter) => { return { items: [] }; }, -);`, - "// setFollowersDispatcher REMOVED", +); + +`, + "", ); assertNoErrors(code); }); @@ -387,8 +441,11 @@ test("Integration: ✅ No setFollowersDispatcher - followers property not requir test("Integration: ✅ No setOutboxDispatcher - outbox property not required", () => { const code = COMPLETE_VALID_CODE .replace( - "outbox: ctx.getOutboxUri(identifier),", - "// outbox: REMOVED", + ` }), + outbox: ctx.getOutboxUri(identifier), + following: ctx.getFollowingUri(identifier),`, + ` }), + following: ctx.getFollowingUri(identifier),`, ) .replace( `federation.setOutboxDispatcher( @@ -396,8 +453,10 @@ test("Integration: ✅ No setOutboxDispatcher - outbox property not required", ( async (_ctx, _identifier, _cursor) => { return { items: [] }; }, -);`, - "// setOutboxDispatcher REMOVED", +); + +`, + "", ); assertNoErrors(code); }); @@ -405,8 +464,11 @@ test("Integration: ✅ No setOutboxDispatcher - outbox property not required", ( test("Integration: ✅ No setLikedDispatcher - liked property not required", () => { const code = COMPLETE_VALID_CODE .replace( - "liked: ctx.getLikedUri(identifier),", - "// liked: REMOVED", + ` followers: ctx.getFollowersUri(identifier), + liked: ctx.getLikedUri(identifier), + featured: ctx.getFeaturedUri(identifier),`, + ` followers: ctx.getFollowersUri(identifier), + featured: ctx.getFeaturedUri(identifier),`, ) .replace( `federation.setLikedDispatcher( @@ -414,8 +476,10 @@ test("Integration: ✅ No setLikedDispatcher - liked property not required", () async (_ctx, _identifier, _cursor) => { return { items: [] }; }, -);`, - "// setLikedDispatcher REMOVED", +); + +`, + "", ); assertNoErrors(code); }); @@ -423,8 +487,11 @@ test("Integration: ✅ No setLikedDispatcher - liked property not required", () test("Integration: ✅ No setFeaturedDispatcher - featured property not required", () => { const code = COMPLETE_VALID_CODE .replace( - "featured: ctx.getFeaturedUri(identifier),", - "// featured: REMOVED", + ` liked: ctx.getLikedUri(identifier), + featured: ctx.getFeaturedUri(identifier), + featuredTags: ctx.getFeaturedTagsUri(identifier),`, + ` liked: ctx.getLikedUri(identifier), + featuredTags: ctx.getFeaturedTagsUri(identifier),`, ) .replace( `federation.setFeaturedDispatcher( @@ -432,8 +499,10 @@ test("Integration: ✅ No setFeaturedDispatcher - featured property not required async (_ctx, _identifier, _cursor) => { return { items: [] }; }, -);`, - "// setFeaturedDispatcher REMOVED", +); + +`, + "", ); assertNoErrors(code); }); @@ -441,8 +510,11 @@ test("Integration: ✅ No setFeaturedDispatcher - featured property not required test("Integration: ✅ No setFeaturedTagsDispatcher - featuredTags property not required", () => { const code = COMPLETE_VALID_CODE .replace( - "featuredTags: ctx.getFeaturedTagsUri(identifier),", - "// featuredTags: REMOVED", + ` featured: ctx.getFeaturedUri(identifier), + featuredTags: ctx.getFeaturedTagsUri(identifier), + publicKey: keyPairs[0]?.cryptographicKey,`, + ` featured: ctx.getFeaturedUri(identifier), + publicKey: keyPairs[0]?.cryptographicKey,`, ) .replace( `federation.setFeaturedTagsDispatcher( @@ -450,8 +522,9 @@ test("Integration: ✅ No setFeaturedTagsDispatcher - featuredTags property not async (_ctx, _identifier, _cursor) => { return { items: [] }; }, -);`, - "// setFeaturedTagsDispatcher REMOVED", +); +`, + "", ); assertNoErrors(code); }); @@ -459,18 +532,20 @@ test("Integration: ✅ No setFeaturedTagsDispatcher - featuredTags property not test("Integration: ✅ No setInboxListeners - inbox/sharedInbox not required", () => { const code = COMPLETE_VALID_CODE .replace( - "inbox: ctx.getInboxUri(identifier),", - "// inbox: REMOVED", - ) - .replace( - `endpoints: new Endpoints({ + ` summary: "A test actor for comprehensive lint rule validation", + inbox: ctx.getInboxUri(identifier), + endpoints: new Endpoints({ sharedInbox: ctx.getInboxUri(), - }),`, - "// endpoints: REMOVED", + }), + outbox: ctx.getOutboxUri(identifier),`, + ` summary: "A test actor for comprehensive lint rule validation", + outbox: ctx.getOutboxUri(identifier),`, ) .replace( - `federation.setInboxListeners("/users/{identifier}/inbox", "/inbox");`, - "// setInboxListeners REMOVED", + `federation.setInboxListeners("/users/{identifier}/inbox", "/inbox"); + +`, + "", ); assertNoErrors(code); }); @@ -478,20 +553,28 @@ test("Integration: ✅ No setInboxListeners - inbox/sharedInbox not required", ( test("Integration: ✅ No setKeyPairsDispatcher - publicKey/assertionMethod not required", () => { const code = COMPLETE_VALID_CODE .replace( - "const keyPairs = await ctx.getActorKeyPairs(identifier);", - "// keyPairs REMOVED", - ) - .replace( - "publicKey: keyPairs[0]?.cryptographicKey,", - "// publicKey: REMOVED", + ` .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + const keyPairs = await ctx.getActorKeyPairs(identifier); + return new Person({`, + ` .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return new Person({`, ) .replace( - "assertionMethod: keyPairs[0]?.multikey,", - "// assertionMethod: REMOVED", + ` featuredTags: ctx.getFeaturedTagsUri(identifier), + publicKey: keyPairs[0]?.cryptographicKey, + assertionMethod: keyPairs[0]?.multikey, + });`, + ` featuredTags: ctx.getFeaturedTagsUri(identifier), + });`, ) .replace( - ".setKeyPairsDispatcher(async (_ctx, _identifier) => []);", - "; // setKeyPairsDispatcher REMOVED", + ` }) + .setKeyPairsDispatcher(async (_ctx, _identifier) => []); + +federation.setInboxListeners`, + ` }); + +federation.setInboxListeners`, ); assertNoErrors(code); }); @@ -511,7 +594,7 @@ test("Integration: ❌ Multiple errors - missing id and inbox", () => { "// inbox: REMOVED", ); - const diagnostics = lintCode(code); + const diagnostics = lintTest(code); const ruleIds = diagnostics.map((d) => d.id); ok( From 5f7b5bd764fdc10a3ae7a2044e99df906567ccf4 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Mon, 15 Dec 2025 09:13:01 +0000 Subject: [PATCH 28/41] Refactor mismatch and required rule factories and rename files --- .../lint/src/lib/mismatch-rule-factory.ts | 213 --------------- packages/lint/src/lib/mismatch.ts | 185 +++++++++++++ .../lint/src/lib/required-rule-factory.ts | 255 ------------------ packages/lint/src/lib/required.ts | 154 +++++++++++ .../rules/actor-assertion-method-required.ts | 2 +- .../rules/actor-featured-property-mismatch.ts | 2 +- .../rules/actor-featured-property-required.ts | 2 +- .../actor-featured-tags-property-mismatch.ts | 2 +- .../actor-featured-tags-property-required.ts | 2 +- .../actor-followers-property-mismatch.ts | 2 +- .../actor-followers-property-required.ts | 2 +- .../actor-following-property-mismatch.ts | 2 +- .../actor-following-property-required.ts | 2 +- packages/lint/src/rules/actor-id-mismatch.ts | 2 +- packages/lint/src/rules/actor-id-required.ts | 2 +- .../rules/actor-inbox-property-mismatch.ts | 2 +- .../rules/actor-inbox-property-required.ts | 2 +- .../rules/actor-liked-property-mismatch.ts | 2 +- .../rules/actor-liked-property-required.ts | 2 +- .../rules/actor-outbox-property-mismatch.ts | 2 +- .../rules/actor-outbox-property-required.ts | 2 +- .../src/rules/actor-public-key-required.ts | 2 +- .../actor-shared-inbox-property-mismatch.ts | 2 +- .../actor-shared-inbox-property-required.ts | 2 +- 24 files changed, 359 insertions(+), 488 deletions(-) delete mode 100644 packages/lint/src/lib/mismatch-rule-factory.ts create mode 100644 packages/lint/src/lib/mismatch.ts delete mode 100644 packages/lint/src/lib/required-rule-factory.ts create mode 100644 packages/lint/src/lib/required.ts diff --git a/packages/lint/src/lib/mismatch-rule-factory.ts b/packages/lint/src/lib/mismatch-rule-factory.ts deleted file mode 100644 index da36d50ad..000000000 --- a/packages/lint/src/lib/mismatch-rule-factory.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { isEmpty, negate, some } from "@fxts/core"; -import type { Rule } from "eslint"; -import { actorPropertyMismatch } from "./messages.ts"; -import { - allOf, - hasMemberExpressionCallee, - isFunction, - isNodeName, - isNodeType, - isSetActorDispatcherCall, -} from "./pred.ts"; -import { - createPropertyChecker, - createPropertySearcher, -} from "./property-checker.ts"; -import { trackFederationVariables } from "./tracker.ts"; -import type { - Expression, - FunctionNode, - Identifier, - MethodCallContext, - Node, - Parameter, - PrivateIdentifier, - PropertyConfig, - SpreadElement, -} from "./types.ts"; - -const isIdentifierWithName = (name: T) => -( - node: Expression | SpreadElement | PrivateIdentifier, -): node is Identifier & { "name": T } => - allOf(isNodeType("Identifier"), isNodeName(name))(node); - -/** - * Checks if a node is a CallExpression calling the expected context method. - */ -const isExpectedMethodCall = ( - { - ctxName, - idName, - methodName, - requiresIdentifier, - }: MethodCallContext, -) => -(node: Node): boolean => { - if ( - !isNodeType("CallExpression")(node) || - !hasMemberExpressionCallee(node) || - !isIdentifierWithName(ctxName)(node.callee.object) || - !isIdentifierWithName(methodName)(node.callee.property) - ) return false; - - if (!requiresIdentifier) return isEmpty(node.arguments); - return allOf( - negate(isEmpty), - some(isIdentifierWithName(idName)), - )(node.arguments); -}; - -/** - * Extracts parameter names from a function. - */ -const extractParams = ( - fn: FunctionNode, -): [string | null, string | null] => { - const params = fn.params; - if (params.length < 2) return [null, null]; - - return params.slice(0, 2).map(getNameIfIdentifier) as [ - string | null, - string | null, - ]; -}; - -const getNameIfIdentifier = (node: Parameter): string | null => - node?.type === "Identifier" ? node.name : null; -/** - * Creates a lint rule that checks if a property uses the correct context method. - * - * @param config Property configuration containing name, getter, setter, and nested info - * @returns A Deno lint rule - */ -export const createMismatchRuleDeno = ( - { path, getter, requiresIdentifier = true }: PropertyConfig, -): Deno.lint.Rule => { - return { - create(context) { - const tracker = trackFederationVariables(); - - return { - VariableDeclarator: tracker.VariableDeclarator, - - CallExpression(node) { - if ( - !isSetActorDispatcherCall(node) || - !hasMemberExpressionCallee(node) || - !tracker.isFederationObject(node.callee.object) - ) return; - - const dispatcherArg = node.arguments[1]; - if (!isFunction(dispatcherArg)) return; - - const [ctxName, idName] = extractParams(dispatcherArg); - if (!ctxName || !idName) return; - - const methodCallContext: MethodCallContext = { - path: path.join("."), - ctxName, - idName, - methodName: getter, - requiresIdentifier, - }; - - const existenceChecker = createPropertyChecker(Boolean); - const hasProperty = createPropertySearcher(existenceChecker(path))( - dispatcherArg.body, - ); - - // If property doesn't exist, don't report (that's for *-required rules) - if (!hasProperty) return; - - // Property exists, now check if the value is correct - const propertyChecker = createPropertyChecker( - isExpectedMethodCall(methodCallContext), - ); - const propertySearcher = createPropertySearcher( - propertyChecker(path), - ); - - if (!propertySearcher(dispatcherArg.body)) { - context.report({ - node: dispatcherArg, - message: actorPropertyMismatch(methodCallContext), - }); - } - }, - }; - }, - }; -}; - -export function createMismatchRuleEslint( - { path, getter, requiresIdentifier = true }: PropertyConfig, -): Rule.RuleModule { - return { - meta: { - type: "problem", - docs: { - description: `Ensure actor's ${ - path.join(".") - } property uses correct context method`, - }, - schema: [], - messages: { - mismatch: "{{ message }}", - }, - }, - create(context) { - const tracker = trackFederationVariables(); - - return { - VariableDeclarator: tracker.VariableDeclarator, - - CallExpression(node) { - if ( - !isSetActorDispatcherCall(node) || - !hasMemberExpressionCallee(node) || - !tracker.isFederationObject(node.callee.object) - ) return; - - const dispatcherArg = node.arguments[1]; - if (!isFunction(dispatcherArg)) return; - - const [ctxName, idName] = extractParams(dispatcherArg); - if (!ctxName || !idName) return; - - const methodCallContext: MethodCallContext = { - path: path.join("."), - ctxName, - idName, - methodName: getter, - requiresIdentifier, - }; - - const existenceChecker = createPropertyChecker(Boolean); - const hasProperty = createPropertySearcher(existenceChecker(path))( - dispatcherArg.body, - ); - - // If property doesn't exist, don't report (that's for *-required rules) - if (!hasProperty) return; - - // Property exists, now check if the value is correct - const propertyChecker = createPropertyChecker( - isExpectedMethodCall(methodCallContext), - ); - const propertySearcher = createPropertySearcher( - propertyChecker(path), - ); - - if (!propertySearcher(dispatcherArg.body)) { - context.report({ - node: dispatcherArg, - messageId: "mismatch", - data: { message: actorPropertyMismatch(methodCallContext) }, - }); - } - }, - }; - }, - }; -} diff --git a/packages/lint/src/lib/mismatch.ts b/packages/lint/src/lib/mismatch.ts new file mode 100644 index 000000000..6ff5f09c3 --- /dev/null +++ b/packages/lint/src/lib/mismatch.ts @@ -0,0 +1,185 @@ +import { isEmpty, negate, some } from "@fxts/core"; +import type { Rule } from "eslint"; +import { actorPropertyMismatch } from "./messages.ts"; +import { + allOf, + hasMemberExpressionCallee, + isFunction, + isNodeName, + isNodeType, + isSetActorDispatcherCall, +} from "./pred.ts"; +import { + createPropertyChecker, + createPropertySearcher, +} from "./property-checker.ts"; +import { trackFederationVariables } from "./tracker.ts"; +import type { + CallExpression, + Expression, + FunctionNode, + Identifier, + MethodCallContext, + Node, + Parameter, + PrivateIdentifier, + PropertyConfig, + SpreadElement, +} from "./types.ts"; + +const isIdentifierWithName = (name: T) => +( + node: Expression | SpreadElement | PrivateIdentifier, +): node is Identifier & { "name": T } => + allOf(isNodeType("Identifier"), isNodeName(name))(node); + +/** + * Checks if a node is a CallExpression calling the expected context method. + */ +const isExpectedMethodCall = ( + { + ctxName, + idName, + methodName, + requiresIdentifier, + }: MethodCallContext, +) => +(node: Node): boolean => { + if ( + !isNodeType("CallExpression")(node) || + !hasMemberExpressionCallee(node) || + !isIdentifierWithName(ctxName)(node.callee.object) || + !isIdentifierWithName(methodName)(node.callee.property) + ) return false; + + if (!requiresIdentifier) return isEmpty(node.arguments); + return allOf( + negate(isEmpty), + some(isIdentifierWithName(idName)), + )(node.arguments); +}; + +/** + * Extracts parameter names from a function. + */ +const extractParams = ( + fn: FunctionNode, +): [string | null, string | null] => { + const params = fn.params; + if (params.length < 2) return [null, null]; + + return params.slice(0, 2).map(getNameIfIdentifier) as [ + string | null, + string | null, + ]; +}; + +const getNameIfIdentifier = (node: Parameter): string | null => + node?.type === "Identifier" ? node.name : null; + +function createMismatchRule( + config: PropertyConfig, + describe: ( + methodCallContext: MethodCallContext, + ) => Context extends Deno.lint.RuleContext ? { + message: string; + } + : { + messageId: string; + data: { message: string }; + }, +) { + return (context: Context) => { + const tracker = trackFederationVariables(); + + return { + VariableDeclarator: tracker.VariableDeclarator, + + CallExpression(node: CallExpression) { + if ( + !isSetActorDispatcherCall(node) || + !hasMemberExpressionCallee(node) || + !tracker.isFederationObject(node.callee.object) + ) return; + + const dispatcherArg = node.arguments[1]; + if (!isFunction(dispatcherArg)) return; + + const [ctxName, idName] = extractParams(dispatcherArg); + if (!ctxName || !idName) return; + + const methodCallContext: MethodCallContext = { + path: config.path.join("."), + ctxName, + idName, + methodName: config.getter, + requiresIdentifier: config.requiresIdentifier, + }; + + const existenceChecker = createPropertyChecker(Boolean)(config.path); + const hasProperty = createPropertySearcher(existenceChecker)( + dispatcherArg.body, + ); + + // If property doesn't exist, don't report (that's for *-required rules) + if (!hasProperty) return; + + // Property exists, now check if the value is correct + const propertyChecker = createPropertyChecker( + isExpectedMethodCall(methodCallContext), + )(config.path); + const propertySearcher = createPropertySearcher( + propertyChecker, + ); + + if (!propertySearcher(dispatcherArg.body)) { + (context as { report: (arg: unknown) => void }).report({ + node: dispatcherArg, + ...describe(methodCallContext), + }); + } + }, + }; + }; +} + +/** + * Creates a lint rule that checks if a property uses the correct context method. + * + * @param config Property configuration containing name, getter, setter, and nested info + * @returns A Deno lint rule + */ +export const createMismatchRuleDeno = ( + config: PropertyConfig, +): Deno.lint.Rule => ({ + create: createMismatchRule( + config, + (context) => ({ + message: actorPropertyMismatch(context), + }), + ), +}); + +export const createMismatchRuleEslint = ( + config: PropertyConfig, +): Rule.RuleModule => ({ + meta: { + type: "problem", + docs: { + description: `Ensure actor's ${ + config.path.join(".") + } property uses correct context method`, + }, + schema: [], + messages: { + mismatch: "{{ message }}", + }, + }, + create: createMismatchRule( + config, + (context) => ({ + messageId: "mismatch", + data: { message: actorPropertyMismatch(context) }, + }), + ), +}); diff --git a/packages/lint/src/lib/required-rule-factory.ts b/packages/lint/src/lib/required-rule-factory.ts deleted file mode 100644 index 1af56ccde..000000000 --- a/packages/lint/src/lib/required-rule-factory.ts +++ /dev/null @@ -1,255 +0,0 @@ -import type { Rule } from "eslint"; -import type * as ESTree from "estree"; -import { actorPropertyRequired } from "./messages.ts"; -import { - allOf, - hasIdentifierProperty, - hasMemberExpressionCallee, - hasMethodName, - isFunction, - isSetActorDispatcherCall, -} from "./pred.ts"; -import { - createPropertyChecker, - createPropertySearcher, -} from "./property-checker.ts"; -import { trackFederationVariables } from "./tracker.ts"; -import type { - CallExpression, - CallMemberExpressionWithIdentified, - PropertyConfig, -} from "./types.ts"; - -/** - * Checks if a CallExpression is a specific dispatcher method call. - */ -const isDispatcherMethodCall = - (methodName: string) => - (node: CallExpression): node is CallMemberExpressionWithIdentified => - allOf( - hasMemberExpressionCallee, - hasIdentifierProperty, - hasMethodName(methodName), - )(node); - -/** - * Tracks dispatcher method calls on federation objects. - */ -const createDispatcherTracker = ( - dispatcherMethod: string, - federationTracker: ReturnType, -) => { - let dispatcherConfigured = false; - - return { - isDispatcherConfigured: () => dispatcherConfigured, - checkDispatcherCall: ( - node: CallExpression, - ) => { - if ( - isDispatcherMethodCall(dispatcherMethod)(node) && - federationTracker.isFederationObject(node.callee.object) - ) { - dispatcherConfigured = true; - } - }, - }; -}; - -/** - * Stores actor dispatcher info for later validation. - */ -interface ActorDispatcherInfoDeno { - node: Deno.lint.CallExpression; - dispatcherArg: - | Deno.lint.ArrowFunctionExpression - | Deno.lint.FunctionExpression; -} - -function _createRequiredRule< - Context = Deno.lint.RuleContext | Rule.RuleContext, - /* CallExpression = Context extends Deno.lint.RuleContext - ? Deno.lint.CallExpression - : TSESTree.CallExpression, */ - ActorDispatcherInfo = Context extends Deno.lint.RuleContext - ? ActorDispatcherInfoDeno - : ActorDispatcherInfoEslint, ->( - config: PropertyConfig, - describe: ( - config: PropertyConfig, - ) => Context extends Deno.lint.RuleContext ? { - message: string; - } - : { - messageId: string; - data: { message: string }; - }, -) { - return (context: Context) => { - const federationTracker = trackFederationVariables(); - const dispatcherTracker = createDispatcherTracker( - config.setter, - federationTracker, - ); - const actorDispatchers: ActorDispatcherInfo[] = []; - - const propertyChecker = createPropertyChecker(Boolean)( - config.path, - ); - const propertySearcher = createPropertySearcher(propertyChecker); - - return { - VariableDeclarator: federationTracker.VariableDeclarator, - - CallExpression(node: CallExpression) { - dispatcherTracker.checkDispatcherCall(node); - - if (!isSetActorDispatcherCall(node)) return; - if (!federationTracker.isFederationObject(node.callee.object)) return; - - const dispatcherArg = node.arguments[1]; - if (isFunction(dispatcherArg)) { - actorDispatchers.push({ node, dispatcherArg } as ActorDispatcherInfo); - } - }, - - "Program:exit"() { - if (!dispatcherTracker.isDispatcherConfigured()) return; - - for ( - const { dispatcherArg } - of actorDispatchers as ActorDispatcherInfoDeno[] - ) { - if (!propertySearcher(dispatcherArg.body)) { - (context as { report: (arg: unknown) => void }).report({ - node: dispatcherArg, - ...describe(config), - }); - } - } - }, - }; - }; -} - -/** - * Creates a required rule with the given property configuration. - */ -export function createRequiredRuleDeno( - config: PropertyConfig, -): Deno.lint.Rule { - return { - create(context) { - const federationTracker = trackFederationVariables(); - const dispatcherTracker = createDispatcherTracker( - config.setter, - federationTracker, - ); - const actorDispatchers: ActorDispatcherInfoDeno[] = []; - - return { - VariableDeclarator: federationTracker.VariableDeclarator, - - CallExpression(node) { - dispatcherTracker.checkDispatcherCall(node); - - if (!isSetActorDispatcherCall(node)) return; - if (!federationTracker.isFederationObject(node.callee.object)) return; - - const dispatcherArg = node.arguments[1]; - if (isFunction(dispatcherArg)) { - actorDispatchers.push({ node, dispatcherArg }); - } - }, - - "Program:exit"() { - if (!dispatcherTracker.isDispatcherConfigured()) return; - - const propertyChecker = createPropertyChecker(Boolean)(config.path); - const propertySearcher = createPropertySearcher(propertyChecker); - - for (const { dispatcherArg } of actorDispatchers) { - if (!propertySearcher(dispatcherArg.body)) { - context.report({ - node: dispatcherArg, - message: actorPropertyRequired(config), - }); - } - } - }, - }; - }, - }; -} - -interface ActorDispatcherInfoEslint { - node: ESTree.CallExpression; - dispatcherArg: - | ESTree.ArrowFunctionExpression - | ESTree.FunctionExpression; -} - -/** - * Creates a required ESLint rule with the given property configuration. - */ - -export function createRequiredRuleEslint( - config: PropertyConfig, -): Rule.RuleModule { - return { - meta: { - type: "suggestion", - docs: { - description: `Ensure actor dispatcher returns ${ - config.path.join(".") - } property`, - }, - schema: [], - messages: { - required: "{{ message }}", - }, - }, - create(context) { - const federationTracker = trackFederationVariables(); - const dispatcherTracker = createDispatcherTracker( - config.setter, - federationTracker, - ); - const actorDispatchers: ActorDispatcherInfoEslint[] = []; - - return { - VariableDeclarator: federationTracker.VariableDeclarator, - - CallExpression(node): void { - dispatcherTracker.checkDispatcherCall(node); - - if (!isSetActorDispatcherCall(node)) return; - if (!federationTracker.isFederationObject(node.callee.object)) return; - - const dispatcherArg = node.arguments[1]; - if (isFunction(dispatcherArg)) { - actorDispatchers.push({ node, dispatcherArg }); - } - }, - - "Program:exit"(): void { - if (!dispatcherTracker.isDispatcherConfigured()) return; - - const propertyChecker = createPropertyChecker(Boolean)(config.path); - const propertySearcher = createPropertySearcher(propertyChecker); - - for (const { dispatcherArg } of actorDispatchers) { - if (!propertySearcher(dispatcherArg.body)) { - context.report({ - node: dispatcherArg, - messageId: "required", - data: { message: actorPropertyRequired(config) }, - }); - } - } - }, - }; - }, - }; -} diff --git a/packages/lint/src/lib/required.ts b/packages/lint/src/lib/required.ts new file mode 100644 index 000000000..8d928de87 --- /dev/null +++ b/packages/lint/src/lib/required.ts @@ -0,0 +1,154 @@ +import type { Rule } from "eslint"; +import { actorPropertyRequired } from "./messages.ts"; +import { + allOf, + hasIdentifierProperty, + hasMemberExpressionCallee, + hasMethodName, + isFunction, + isSetActorDispatcherCall, +} from "./pred.ts"; +import { + createPropertyChecker, + createPropertySearcher, +} from "./property-checker.ts"; +import { trackFederationVariables } from "./tracker.ts"; +import type { + CallExpression, + CallMemberExpressionWithIdentified, + FunctionNode, + PropertyConfig, +} from "./types.ts"; + +/** + * Checks if a CallExpression is a specific dispatcher method call. + */ +const isDispatcherMethodCall = + (methodName: string) => + (node: CallExpression): node is CallMemberExpressionWithIdentified => + allOf( + hasMemberExpressionCallee, + hasIdentifierProperty, + hasMethodName(methodName), + )(node); + +/** + * Tracks dispatcher method calls on federation objects. + */ +const createDispatcherTracker = ( + dispatcherMethod: string, + federationTracker: ReturnType, +) => { + let dispatcherConfigured = false; + + return { + isDispatcherConfigured: () => dispatcherConfigured, + checkDispatcherCall: ( + node: CallExpression, + ) => { + if ( + isDispatcherMethodCall(dispatcherMethod)(node) && + federationTracker.isFederationObject(node.callee.object) + ) { + dispatcherConfigured = true; + } + }, + }; +}; + +function createRequiredRule( + config: PropertyConfig, + description: Context extends Deno.lint.RuleContext ? { + message: string; + } + : { + messageId: string; + data: { message: string }; + }, +) { + return (context: Context) => { + const federationTracker = trackFederationVariables(); + const dispatcherTracker = createDispatcherTracker( + config.setter, + federationTracker, + ); + const actorDispatchers: FunctionNode[] = []; + + const propertyChecker = createPropertyChecker(Boolean)( + config.path, + ); + const propertySearcher = createPropertySearcher(propertyChecker); + + return { + VariableDeclarator: federationTracker.VariableDeclarator, + + CallExpression(node: CallExpression) { + dispatcherTracker.checkDispatcherCall(node); + + if (!isSetActorDispatcherCall(node)) return; + if (!federationTracker.isFederationObject(node.callee.object)) return; + + const dispatcher = node.arguments[1]; + if (isFunction(dispatcher)) { + actorDispatchers.push(dispatcher); + } + }, + + "Program:exit"() { + if (!dispatcherTracker.isDispatcherConfigured()) return; + + for (const dispatcher of actorDispatchers) { + if (!propertySearcher(dispatcher.body)) { + (context as { report: (arg: unknown) => void }).report({ + node: dispatcher, + ...description, + }); + } + } + }, + }; + }; +} + +/** + * Creates a required rule with the given property configuration. + */ +export function createRequiredRuleDeno( + config: PropertyConfig, +): Deno.lint.Rule { + return { + create: createRequiredRule( + config, + { message: actorPropertyRequired(config) }, + ), + }; +} + +/** + * Creates a required ESLint rule with the given property configuration. + */ +export function createRequiredRuleEslint( + config: PropertyConfig, +): Rule.RuleModule { + return { + meta: { + type: "suggestion", + docs: { + description: `Ensure actor dispatcher returns ${ + config.path.join(".") + } property.`, + }, + schema: [], + messages: { + required: "{{ message }}", + }, + }, + create: createRequiredRule( + config, + { + messageId: "required", + data: { message: actorPropertyRequired(config) }, + }, + ), + }; +} diff --git a/packages/lint/src/rules/actor-assertion-method-required.ts b/packages/lint/src/rules/actor-assertion-method-required.ts index 3c05225ff..86404938f 100644 --- a/packages/lint/src/rules/actor-assertion-method-required.ts +++ b/packages/lint/src/rules/actor-assertion-method-required.ts @@ -2,7 +2,7 @@ import { properties } from "../lib/const.ts"; import { createRequiredRuleDeno, createRequiredRuleEslint, -} from "../lib/required-rule-factory.ts"; +} from "../lib/required.ts"; export const deno = createRequiredRuleDeno( properties.assertionMethod, diff --git a/packages/lint/src/rules/actor-featured-property-mismatch.ts b/packages/lint/src/rules/actor-featured-property-mismatch.ts index fa1f43940..f47cf8047 100644 --- a/packages/lint/src/rules/actor-featured-property-mismatch.ts +++ b/packages/lint/src/rules/actor-featured-property-mismatch.ts @@ -2,7 +2,7 @@ import { properties } from "../lib/const.ts"; import { createMismatchRuleDeno, createMismatchRuleEslint, -} from "../lib/mismatch-rule-factory.ts"; +} from "../lib/mismatch.ts"; export const deno = createMismatchRuleDeno( properties.featured, diff --git a/packages/lint/src/rules/actor-featured-property-required.ts b/packages/lint/src/rules/actor-featured-property-required.ts index 8e268c78d..1637c158b 100644 --- a/packages/lint/src/rules/actor-featured-property-required.ts +++ b/packages/lint/src/rules/actor-featured-property-required.ts @@ -2,7 +2,7 @@ import { properties } from "../lib/const.ts"; import { createRequiredRuleDeno, createRequiredRuleEslint, -} from "../lib/required-rule-factory.ts"; +} from "../lib/required.ts"; export const deno = createRequiredRuleDeno(properties.featured); export const eslint = createRequiredRuleEslint(properties.featured); diff --git a/packages/lint/src/rules/actor-featured-tags-property-mismatch.ts b/packages/lint/src/rules/actor-featured-tags-property-mismatch.ts index a27f892b2..ed0e33eb2 100644 --- a/packages/lint/src/rules/actor-featured-tags-property-mismatch.ts +++ b/packages/lint/src/rules/actor-featured-tags-property-mismatch.ts @@ -2,7 +2,7 @@ import { properties } from "../lib/const.ts"; import { createMismatchRuleDeno, createMismatchRuleEslint, -} from "../lib/mismatch-rule-factory.ts"; +} from "../lib/mismatch.ts"; export const deno = createMismatchRuleDeno( properties.featuredTags, diff --git a/packages/lint/src/rules/actor-featured-tags-property-required.ts b/packages/lint/src/rules/actor-featured-tags-property-required.ts index 13988a42a..3efddecda 100644 --- a/packages/lint/src/rules/actor-featured-tags-property-required.ts +++ b/packages/lint/src/rules/actor-featured-tags-property-required.ts @@ -2,7 +2,7 @@ import { properties } from "../lib/const.ts"; import { createRequiredRuleDeno, createRequiredRuleEslint, -} from "../lib/required-rule-factory.ts"; +} from "../lib/required.ts"; export const deno = createRequiredRuleDeno( properties.featuredTags, diff --git a/packages/lint/src/rules/actor-followers-property-mismatch.ts b/packages/lint/src/rules/actor-followers-property-mismatch.ts index d2021cc44..6fd2ed7c7 100644 --- a/packages/lint/src/rules/actor-followers-property-mismatch.ts +++ b/packages/lint/src/rules/actor-followers-property-mismatch.ts @@ -2,7 +2,7 @@ import { properties } from "../lib/const.ts"; import { createMismatchRuleDeno, createMismatchRuleEslint, -} from "../lib/mismatch-rule-factory.ts"; +} from "../lib/mismatch.ts"; export const deno = createMismatchRuleDeno( properties.followers, diff --git a/packages/lint/src/rules/actor-followers-property-required.ts b/packages/lint/src/rules/actor-followers-property-required.ts index bea9dcfeb..4b06e148c 100644 --- a/packages/lint/src/rules/actor-followers-property-required.ts +++ b/packages/lint/src/rules/actor-followers-property-required.ts @@ -2,7 +2,7 @@ import { properties } from "../lib/const.ts"; import { createRequiredRuleDeno, createRequiredRuleEslint, -} from "../lib/required-rule-factory.ts"; +} from "../lib/required.ts"; export const deno = createRequiredRuleDeno( properties.followers, diff --git a/packages/lint/src/rules/actor-following-property-mismatch.ts b/packages/lint/src/rules/actor-following-property-mismatch.ts index 4f710c91c..1accdee8e 100644 --- a/packages/lint/src/rules/actor-following-property-mismatch.ts +++ b/packages/lint/src/rules/actor-following-property-mismatch.ts @@ -2,7 +2,7 @@ import { properties } from "../lib/const.ts"; import { createMismatchRuleDeno, createMismatchRuleEslint, -} from "../lib/mismatch-rule-factory.ts"; +} from "../lib/mismatch.ts"; export const deno = createMismatchRuleDeno( properties.following, diff --git a/packages/lint/src/rules/actor-following-property-required.ts b/packages/lint/src/rules/actor-following-property-required.ts index 7006d5d46..38619e7b1 100644 --- a/packages/lint/src/rules/actor-following-property-required.ts +++ b/packages/lint/src/rules/actor-following-property-required.ts @@ -2,7 +2,7 @@ import { properties } from "../lib/const.ts"; import { createRequiredRuleDeno, createRequiredRuleEslint, -} from "../lib/required-rule-factory.ts"; +} from "../lib/required.ts"; export const deno = createRequiredRuleDeno( properties.following, diff --git a/packages/lint/src/rules/actor-id-mismatch.ts b/packages/lint/src/rules/actor-id-mismatch.ts index 01d55c783..f4203ca0d 100644 --- a/packages/lint/src/rules/actor-id-mismatch.ts +++ b/packages/lint/src/rules/actor-id-mismatch.ts @@ -2,7 +2,7 @@ import { properties } from "../lib/const.ts"; import { createMismatchRuleDeno, createMismatchRuleEslint, -} from "../lib/mismatch-rule-factory.ts"; +} from "../lib/mismatch.ts"; export const deno = createMismatchRuleDeno(properties.id); export const eslint = createMismatchRuleEslint(properties.id); diff --git a/packages/lint/src/rules/actor-id-required.ts b/packages/lint/src/rules/actor-id-required.ts index c5814a815..7d4318e52 100644 --- a/packages/lint/src/rules/actor-id-required.ts +++ b/packages/lint/src/rules/actor-id-required.ts @@ -2,7 +2,7 @@ import { properties } from "../lib/const.ts"; import { createRequiredRuleDeno, createRequiredRuleEslint, -} from "../lib/required-rule-factory.ts"; +} from "../lib/required.ts"; export const deno = createRequiredRuleDeno(properties.id); export const eslint = createRequiredRuleEslint(properties.id); diff --git a/packages/lint/src/rules/actor-inbox-property-mismatch.ts b/packages/lint/src/rules/actor-inbox-property-mismatch.ts index b71472405..a1f2c159d 100644 --- a/packages/lint/src/rules/actor-inbox-property-mismatch.ts +++ b/packages/lint/src/rules/actor-inbox-property-mismatch.ts @@ -2,7 +2,7 @@ import { properties } from "../lib/const.ts"; import { createMismatchRuleDeno, createMismatchRuleEslint, -} from "../lib/mismatch-rule-factory.ts"; +} from "../lib/mismatch.ts"; export const deno = createMismatchRuleDeno( properties.inbox, diff --git a/packages/lint/src/rules/actor-inbox-property-required.ts b/packages/lint/src/rules/actor-inbox-property-required.ts index 862cc7dac..33b794dea 100644 --- a/packages/lint/src/rules/actor-inbox-property-required.ts +++ b/packages/lint/src/rules/actor-inbox-property-required.ts @@ -2,7 +2,7 @@ import { properties } from "../lib/const.ts"; import { createRequiredRuleDeno, createRequiredRuleEslint, -} from "../lib/required-rule-factory.ts"; +} from "../lib/required.ts"; export const deno = createRequiredRuleDeno( properties.inbox, diff --git a/packages/lint/src/rules/actor-liked-property-mismatch.ts b/packages/lint/src/rules/actor-liked-property-mismatch.ts index ad3833eda..648c1f868 100644 --- a/packages/lint/src/rules/actor-liked-property-mismatch.ts +++ b/packages/lint/src/rules/actor-liked-property-mismatch.ts @@ -2,7 +2,7 @@ import { properties } from "../lib/const.ts"; import { createMismatchRuleDeno, createMismatchRuleEslint, -} from "../lib/mismatch-rule-factory.ts"; +} from "../lib/mismatch.ts"; export const deno = createMismatchRuleDeno( properties.liked, diff --git a/packages/lint/src/rules/actor-liked-property-required.ts b/packages/lint/src/rules/actor-liked-property-required.ts index 000135c40..31cd7d541 100644 --- a/packages/lint/src/rules/actor-liked-property-required.ts +++ b/packages/lint/src/rules/actor-liked-property-required.ts @@ -2,7 +2,7 @@ import { properties } from "../lib/const.ts"; import { createRequiredRuleDeno, createRequiredRuleEslint, -} from "../lib/required-rule-factory.ts"; +} from "../lib/required.ts"; export const deno = createRequiredRuleDeno( properties.liked, diff --git a/packages/lint/src/rules/actor-outbox-property-mismatch.ts b/packages/lint/src/rules/actor-outbox-property-mismatch.ts index 515b5a08a..aa4698fb1 100644 --- a/packages/lint/src/rules/actor-outbox-property-mismatch.ts +++ b/packages/lint/src/rules/actor-outbox-property-mismatch.ts @@ -2,7 +2,7 @@ import { properties } from "../lib/const.ts"; import { createMismatchRuleDeno, createMismatchRuleEslint, -} from "../lib/mismatch-rule-factory.ts"; +} from "../lib/mismatch.ts"; export const deno = createMismatchRuleDeno( properties.outbox, diff --git a/packages/lint/src/rules/actor-outbox-property-required.ts b/packages/lint/src/rules/actor-outbox-property-required.ts index bf75a95fd..0f8866ea8 100644 --- a/packages/lint/src/rules/actor-outbox-property-required.ts +++ b/packages/lint/src/rules/actor-outbox-property-required.ts @@ -2,7 +2,7 @@ import { properties } from "../lib/const.ts"; import { createRequiredRuleDeno, createRequiredRuleEslint, -} from "../lib/required-rule-factory.ts"; +} from "../lib/required.ts"; export const deno = createRequiredRuleDeno( properties.outbox, diff --git a/packages/lint/src/rules/actor-public-key-required.ts b/packages/lint/src/rules/actor-public-key-required.ts index 0068b226b..89f5fe35d 100644 --- a/packages/lint/src/rules/actor-public-key-required.ts +++ b/packages/lint/src/rules/actor-public-key-required.ts @@ -2,7 +2,7 @@ import { properties } from "../lib/const.ts"; import { createRequiredRuleDeno, createRequiredRuleEslint, -} from "../lib/required-rule-factory.ts"; +} from "../lib/required.ts"; export const deno = createRequiredRuleDeno( properties.publicKey, diff --git a/packages/lint/src/rules/actor-shared-inbox-property-mismatch.ts b/packages/lint/src/rules/actor-shared-inbox-property-mismatch.ts index bfbe8fc1c..544d1bddc 100644 --- a/packages/lint/src/rules/actor-shared-inbox-property-mismatch.ts +++ b/packages/lint/src/rules/actor-shared-inbox-property-mismatch.ts @@ -2,7 +2,7 @@ import { properties } from "../lib/const.ts"; import { createMismatchRuleDeno, createMismatchRuleEslint, -} from "../lib/mismatch-rule-factory.ts"; +} from "../lib/mismatch.ts"; export const deno = createMismatchRuleDeno( properties.sharedInbox, diff --git a/packages/lint/src/rules/actor-shared-inbox-property-required.ts b/packages/lint/src/rules/actor-shared-inbox-property-required.ts index ee70bb1be..7b09a74cc 100644 --- a/packages/lint/src/rules/actor-shared-inbox-property-required.ts +++ b/packages/lint/src/rules/actor-shared-inbox-property-required.ts @@ -2,7 +2,7 @@ import { properties } from "../lib/const.ts"; import { createRequiredRuleDeno, createRequiredRuleEslint, -} from "../lib/required-rule-factory.ts"; +} from "../lib/required.ts"; export const deno = createRequiredRuleDeno( properties.sharedInbox, From 727c99a5eb06ac5caccf37976e1738776fcd9507 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Mon, 15 Dec 2025 10:00:14 +0000 Subject: [PATCH 29/41] chore and comment things --- deno.json | 2 +- deno.lock | 532 +++++++++++++++++- packages/lint/deno.json | 2 +- packages/lint/package.json | 2 +- packages/lint/src/lib/messages.ts | 6 +- packages/lint/src/lib/mismatch.ts | 6 +- packages/lint/src/lib/pred.ts | 3 +- packages/lint/src/lib/property-checker.ts | 6 +- packages/lint/src/lib/test-templates.ts | 127 +++-- .../collection-filtering-not-implemented.ts | 8 +- packages/lint/tsdown.config.ts | 1 - pnpm-lock.yaml | 192 ++++++- pnpm-workspace.yaml | 2 +- 13 files changed, 788 insertions(+), 101 deletions(-) diff --git a/deno.json b/deno.json index ccf76af21..4f2987653 100644 --- a/deno.json +++ b/deno.json @@ -40,7 +40,7 @@ "@types/node": "npm:@types/node@^22.16.0", "amqplib": "npm:amqplib@^0.10.8", "byte-encodings": "npm:byte-encodings@^1.0.11", - "es-toolkit": "npm:es-toolkit@^1.39.10", + "es-toolkit": "npm:es-toolkit@^1.42.0", "h3": "npm:h3@^1.15.0", "ioredis": "npm:ioredis@^5.6.1", "json-preserve-indent": "npm:json-preserve-indent@^1.1.3", diff --git a/deno.lock b/deno.lock index 0c38af8e4..ffa195ab2 100644 --- a/deno.lock +++ b/deno.lock @@ -26,8 +26,7 @@ "npm:@alinea/suite@~0.6.3": "0.6.3", "npm:@cfworker/json-schema@^4.1.1": "4.1.1", "npm:@cloudflare/workers-types@^4.20250529.0": "4.20251219.0", - "npm:@fxts/core@^1.15.0": "1.21.1", - "npm:@fxts/core@^1.20.0": "1.21.1", + "npm:@fxts/core@^1.21.1": "1.21.1", "npm:@hongminhee/localtunnel@0.3": "0.3.0", "npm:@inquirer/prompts@^7.8.4": "7.10.1_@types+node@22.19.3", "npm:@jimp/core@^1.6.0": "1.6.0", @@ -44,8 +43,12 @@ "npm:@poppanator/http-constants@^1.1.1": "1.1.1", "npm:@sveltejs/kit@2": "2.49.2_@opentelemetry+api@1.9.0_@sveltejs+vite-plugin-svelte@6.2.1__svelte@5.46.0___acorn@8.15.0__vite@7.3.0___@types+node@22.19.3___tsx@4.21.0___yaml@2.8.2___picomatch@4.0.3__@types+node@22.19.3__tsx@4.21.0__yaml@2.8.2_svelte@5.46.0__acorn@8.15.0_vite@7.3.0__@types+node@22.19.3__tsx@4.21.0__yaml@2.8.2__picomatch@4.0.3_acorn@8.15.0_@types+node@22.19.3_tsx@4.21.0_yaml@2.8.2", "npm:@types/amqplib@~0.10.7": "0.10.8", + "npm:@types/eslint@9": "9.6.1", + "npm:@types/estree@^1.0.8": "1.0.8", "npm:@types/node@^22.16.0": "22.19.3", "npm:@types/node@^24.2.1": "24.10.4", + "npm:@typescript-eslint/parser@^8.49.0": "8.50.0_eslint@9.39.2_typescript@5.9.3", + "npm:@typescript-eslint/utils@8": "8.50.0_eslint@9.39.2_typescript@5.9.3", "npm:amqplib@~0.10.8": "0.10.9", "npm:asn1js@^3.0.5": "3.0.7", "npm:asn1js@^3.0.6": "3.0.7", @@ -59,6 +62,8 @@ "npm:es-toolkit@^1.31.0": "1.43.0", "npm:es-toolkit@^1.39.10": "1.43.0", "npm:es-toolkit@^1.39.5": "1.43.0", + "npm:es-toolkit@^1.42.0": "1.43.0", + "npm:eslint@9": "9.39.2", "npm:express@4": "4.22.1", "npm:fast-check@^3.22.0": "3.23.2", "npm:fastify-plugin@^5.0.1": "5.1.0", @@ -431,10 +436,67 @@ "os": ["win32"], "cpu": ["x64"] }, + "@eslint-community/eslint-utils@4.9.0_eslint@9.39.2": { + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dependencies": [ + "eslint", + "eslint-visitor-keys@3.4.3" + ] + }, + "@eslint-community/regexpp@4.12.2": { + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==" + }, + "@eslint/config-array@0.21.1": { + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dependencies": [ + "@eslint/object-schema", + "debug@4.4.3", + "minimatch@3.1.2" + ] + }, + "@eslint/config-helpers@0.4.2": { + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dependencies": [ + "@eslint/core" + ] + }, + "@eslint/core@0.17.0": { + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dependencies": [ + "@types/json-schema" + ] + }, + "@eslint/eslintrc@3.3.3": { + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dependencies": [ + "ajv@6.12.6", + "debug@4.4.3", + "espree", + "globals", + "ignore", + "import-fresh", + "js-yaml", + "minimatch@3.1.2", + "strip-json-comments" + ] + }, + "@eslint/js@9.39.2": { + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==" + }, + "@eslint/object-schema@2.1.7": { + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==" + }, + "@eslint/plugin-kit@0.4.1": { + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dependencies": [ + "@eslint/core", + "levn" + ] + }, "@fastify/ajv-compiler@4.0.5_ajv@8.17.1": { "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==", "dependencies": [ - "ajv", + "ajv@8.17.1", "ajv-formats", "fast-uri" ] @@ -476,6 +538,22 @@ "@logtape/logtape" ] }, + "@humanfs/core@0.19.1": { + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==" + }, + "@humanfs/node@0.16.7": { + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dependencies": [ + "@humanfs/core", + "@humanwhocodes/retry" + ] + }, + "@humanwhocodes/module-importer@1.0.1": { + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==" + }, + "@humanwhocodes/retry@0.4.3": { + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==" + }, "@img/sharp-darwin-arm64@0.33.5": { "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", "optionalDependencies": [ @@ -1563,6 +1641,13 @@ "@types/cookie@0.6.0": { "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" }, + "@types/eslint@9.6.1": { + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dependencies": [ + "@types/estree", + "@types/json-schema" + ] + }, "@types/estree@1.0.8": { "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" }, @@ -1575,6 +1660,9 @@ "@types/unist" ] }, + "@types/json-schema@7.0.15": { + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, "@types/mdast@4.0.4": { "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", "dependencies": [ @@ -1620,6 +1708,76 @@ "@types/wrap-ansi@3.0.0": { "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==" }, + "@typescript-eslint/parser@8.50.0_eslint@9.39.2_typescript@5.9.3": { + "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", + "dependencies": [ + "@typescript-eslint/scope-manager", + "@typescript-eslint/types", + "@typescript-eslint/typescript-estree", + "@typescript-eslint/visitor-keys", + "debug@4.4.3", + "eslint", + "typescript" + ] + }, + "@typescript-eslint/project-service@8.50.0_typescript@5.9.3": { + "integrity": "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==", + "dependencies": [ + "@typescript-eslint/tsconfig-utils", + "@typescript-eslint/types", + "debug@4.4.3", + "typescript" + ] + }, + "@typescript-eslint/scope-manager@8.50.0": { + "integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==", + "dependencies": [ + "@typescript-eslint/types", + "@typescript-eslint/visitor-keys" + ] + }, + "@typescript-eslint/tsconfig-utils@8.50.0_typescript@5.9.3": { + "integrity": "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==", + "dependencies": [ + "typescript" + ] + }, + "@typescript-eslint/types@8.50.0": { + "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==" + }, + "@typescript-eslint/typescript-estree@8.50.0_typescript@5.9.3": { + "integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==", + "dependencies": [ + "@typescript-eslint/project-service", + "@typescript-eslint/tsconfig-utils", + "@typescript-eslint/types", + "@typescript-eslint/visitor-keys", + "debug@4.4.3", + "minimatch@9.0.5", + "semver", + "tinyglobby", + "ts-api-utils", + "typescript" + ] + }, + "@typescript-eslint/utils@8.50.0_eslint@9.39.2_typescript@5.9.3": { + "integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==", + "dependencies": [ + "@eslint-community/eslint-utils", + "@typescript-eslint/scope-manager", + "@typescript-eslint/types", + "@typescript-eslint/typescript-estree", + "eslint", + "typescript" + ] + }, + "@typescript-eslint/visitor-keys@8.50.0": { + "integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==", + "dependencies": [ + "@typescript-eslint/types", + "eslint-visitor-keys@4.2.1" + ] + }, "@ungap/structured-clone@1.3.0": { "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==" }, @@ -1639,6 +1797,12 @@ "negotiator" ] }, + "acorn-jsx@5.3.2_acorn@8.15.0": { + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dependencies": [ + "acorn@8.15.0" + ] + }, "acorn-walk@8.3.2": { "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==" }, @@ -1653,10 +1817,19 @@ "ajv-formats@3.0.1_ajv@8.17.1": { "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "dependencies": [ - "ajv" + "ajv@8.17.1" ], "optionalPeers": [ - "ajv" + "ajv@8.17.1" + ] + }, + "ajv@6.12.6": { + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": [ + "fast-deep-equal", + "fast-json-stable-stringify", + "json-schema-traverse@0.4.1", + "uri-js" ] }, "ajv@8.17.1": { @@ -1664,7 +1837,7 @@ "dependencies": [ "fast-deep-equal", "fast-uri", - "json-schema-traverse", + "json-schema-traverse@1.0.0", "require-from-string" ] }, @@ -1705,6 +1878,9 @@ "any-promise@1.3.0": { "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" }, + "argparse@2.0.1": { + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, "aria-query@5.3.2": { "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==" }, @@ -1742,6 +1918,9 @@ "axobject-query@4.1.0": { "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==" }, + "balanced-match@1.0.2": { + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, "base64-js@1.5.1": { "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, @@ -1774,6 +1953,19 @@ "unpipe" ] }, + "brace-expansion@1.1.12": { + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dependencies": [ + "balanced-match", + "concat-map" + ] + }, + "brace-expansion@2.0.2": { + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dependencies": [ + "balanced-match" + ] + }, "buffer-more-ints@1.0.0": { "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==" }, @@ -1824,6 +2016,9 @@ "get-intrinsic" ] }, + "callsites@3.1.0": { + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + }, "canonicalize@2.1.0": { "integrity": "sha512-F705O3xrsUtgt98j7leetNhTWPe+5S72rlL5O4jA1pKqBVQ/dT1O1D6PFxmSXvc0SUOinWS57DKx0I3CHrXJHQ==", "bin": true @@ -1932,6 +2127,9 @@ "comma-separated-tokens@2.0.3": { "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==" }, + "concat-map@0.0.1": { + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, "content-disposition@0.5.4": { "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "dependencies": [ @@ -1963,6 +2161,14 @@ "keygrip" ] }, + "cross-spawn@7.0.6": { + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dependencies": [ + "path-key", + "shebang-command", + "which" + ] + }, "crossws@0.3.5": { "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", "dependencies": [ @@ -1999,6 +2205,9 @@ "deep-equal@1.0.1": { "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==" }, + "deep-is@0.1.4": { + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + }, "deepmerge@4.3.1": { "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" }, @@ -2140,15 +2349,97 @@ "escape-html@1.0.3": { "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, + "escape-string-regexp@4.0.0": { + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, + "eslint-scope@8.4.0": { + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dependencies": [ + "esrecurse", + "estraverse" + ] + }, + "eslint-visitor-keys@3.4.3": { + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==" + }, + "eslint-visitor-keys@4.2.1": { + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==" + }, + "eslint@9.39.2": { + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dependencies": [ + "@eslint-community/eslint-utils", + "@eslint-community/regexpp", + "@eslint/config-array", + "@eslint/config-helpers", + "@eslint/core", + "@eslint/eslintrc", + "@eslint/js", + "@eslint/plugin-kit", + "@humanfs/node", + "@humanwhocodes/module-importer", + "@humanwhocodes/retry", + "@types/estree", + "ajv@6.12.6", + "chalk@4.1.2", + "cross-spawn", + "debug@4.4.3", + "escape-string-regexp", + "eslint-scope", + "eslint-visitor-keys@4.2.1", + "espree", + "esquery", + "esutils", + "fast-deep-equal", + "file-entry-cache", + "find-up", + "glob-parent", + "ignore", + "imurmurhash", + "is-glob", + "json-stable-stringify-without-jsonify", + "lodash.merge", + "minimatch@3.1.2", + "natural-compare", + "optionator" + ], + "bin": true + }, "esm-env@1.2.2": { "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==" }, + "espree@10.4.0_acorn@8.15.0": { + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dependencies": [ + "acorn@8.15.0", + "acorn-jsx", + "eslint-visitor-keys@4.2.1" + ] + }, + "esquery@1.6.0": { + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dependencies": [ + "estraverse" + ] + }, "esrap@2.2.1": { "integrity": "sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg==", "dependencies": [ "@jridgewell/sourcemap-codec" ] }, + "esrecurse@4.3.0": { + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dependencies": [ + "estraverse" + ] + }, + "estraverse@5.3.0": { + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + }, + "esutils@2.0.3": { + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, "etag@1.8.1": { "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, @@ -2212,17 +2503,23 @@ "fast-deep-equal@3.1.3": { "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "fast-json-stable-stringify@2.1.0": { + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, "fast-json-stringify@6.1.1_ajv@8.17.1": { "integrity": "sha512-DbgptncYEXZqDUOEl4krff4mUiVrTZZVI7BBrQR/T3BqMj/eM1flTC1Uk2uUoLcWCxjT95xKulV/Lc6hhOZsBQ==", "dependencies": [ "@fastify/merge-json-schemas", - "ajv", + "ajv@8.17.1", "ajv-formats", "fast-uri", "json-schema-ref-resolver", "rfdc" ] }, + "fast-levenshtein@2.0.6": { + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + }, "fast-querystring@1.1.2": { "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", "dependencies": [ @@ -2282,6 +2579,12 @@ "fflate@0.8.2": { "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==" }, + "file-entry-cache@8.0.0": { + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dependencies": [ + "flat-cache" + ] + }, "file-type@16.5.4": { "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", "dependencies": [ @@ -2328,6 +2631,23 @@ "safe-regex2" ] }, + "find-up@5.0.0": { + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dependencies": [ + "locate-path", + "path-exists" + ] + }, + "flat-cache@4.0.1": { + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dependencies": [ + "flatted", + "keyv" + ] + }, + "flatted@3.3.3": { + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==" + }, "forwarded@0.2.0": { "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" }, @@ -2393,9 +2713,18 @@ "omggif" ] }, + "glob-parent@6.0.2": { + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dependencies": [ + "is-glob" + ] + }, "glob-to-regexp@0.4.1": { "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" }, + "globals@14.0.0": { + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==" + }, "gopd@1.2.0": { "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" }, @@ -2518,12 +2847,25 @@ "ieee754@1.2.1": { "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, + "ignore@5.3.2": { + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==" + }, "image-q@4.0.0": { "integrity": "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==", "dependencies": [ "@types/node@16.9.1" ] }, + "import-fresh@3.3.1": { + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dependencies": [ + "parent-module", + "resolve-from" + ] + }, + "imurmurhash@0.1.4": { + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" + }, "inherits@2.0.4": { "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, @@ -2575,6 +2917,9 @@ "is-arrayish@0.3.4": { "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==" }, + "is-extglob@2.1.1": { + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + }, "is-fullwidth-code-point@3.0.0": { "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, @@ -2588,6 +2933,12 @@ "safe-regex-test" ] }, + "is-glob@4.0.3": { + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": [ + "is-extglob" + ] + }, "is-interactive@2.0.0": { "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==" }, @@ -2615,6 +2966,9 @@ "is-unicode-supported@2.1.0": { "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==" }, + "isexe@2.0.0": { + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, "isomorphic-fetch@3.0.0": { "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", "dependencies": [ @@ -2664,6 +3018,13 @@ "jpeg-js@0.4.4": { "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==" }, + "js-yaml@4.1.1": { + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dependencies": [ + "argparse" + ], + "bin": true + }, "jsbi@4.3.2": { "integrity": "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==" }, @@ -2671,6 +3032,9 @@ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "bin": true }, + "json-buffer@3.0.1": { + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, "json-canon@1.0.1": { "integrity": "sha512-PQcj4PFOTAQxE8PgoQ4KrM0DcKWZd7S3ELOON8rmysl9I8JuFMgxu1H9v+oZsTPjjkpeS3IHPwLjr7d+gKygnw==" }, @@ -2686,9 +3050,15 @@ "dequal" ] }, + "json-schema-traverse@0.4.1": { + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, "json-schema-traverse@1.0.0": { "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, + "json-stable-stringify-without-jsonify@1.0.1": { + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + }, "jsonld@9.0.0": { "integrity": "sha512-pjMIdkXfC1T2wrX9B9i2uXhGdyCmgec3qgMht+TDj+S0qX3bjWMQUfL7NeqEhuRTi8G5ESzmL9uGlST7nzSEWg==", "dependencies": [ @@ -2705,6 +3075,12 @@ ], "deprecated": true }, + "keyv@4.5.4": { + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dependencies": [ + "json-buffer" + ] + }, "kleur@4.1.5": { "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==" }, @@ -2749,6 +3125,13 @@ "ky@1.14.1": { "integrity": "sha512-hYje4L9JCmpEQBtudo+v52X5X8tgWXUYyPcxKSuxQNboqufecl9VMWjGiucAFH060AwPXHZuH+WB2rrqfkmafw==" }, + "levn@0.4.1": { + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dependencies": [ + "prelude-ls", + "type-check" + ] + }, "light-my-request@6.6.0": { "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", "dependencies": [ @@ -2763,12 +3146,21 @@ "locate-character@3.0.0": { "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==" }, + "locate-path@6.0.0": { + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dependencies": [ + "p-locate" + ] + }, "lodash.defaults@4.2.0": { "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" }, "lodash.isarguments@3.1.0": { "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" }, + "lodash.merge@4.6.2": { + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, "lodash@4.17.21": { "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, @@ -2879,6 +3271,18 @@ ], "bin": true }, + "minimatch@3.1.2": { + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": [ + "brace-expansion@1.1.12" + ] + }, + "minimatch@9.0.5": { + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": [ + "brace-expansion@2.0.2" + ] + }, "mri@1.2.0": { "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==" }, @@ -2920,6 +3324,9 @@ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "bin": true }, + "natural-compare@1.4.0": { + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + }, "negotiator@0.6.3": { "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" }, @@ -2967,6 +3374,17 @@ "only@0.0.2": { "integrity": "sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==" }, + "optionator@0.9.4": { + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dependencies": [ + "deep-is", + "fast-levenshtein", + "levn", + "prelude-ls", + "type-check", + "word-wrap" + ] + }, "ora@8.2.0": { "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", "dependencies": [ @@ -2981,9 +3399,27 @@ "strip-ansi@7.1.2" ] }, + "p-limit@3.1.0": { + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dependencies": [ + "yocto-queue" + ] + }, + "p-locate@5.0.0": { + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dependencies": [ + "p-limit" + ] + }, "pako@1.0.11": { "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, + "parent-module@1.0.1": { + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": [ + "callsites" + ] + }, "parse-bmfont-ascii@1.0.6": { "integrity": "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==" }, @@ -3012,6 +3448,12 @@ "parseurl@1.3.3": { "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "path-exists@4.0.0": { + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "path-key@3.1.1": { + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, "path-to-regexp@0.1.12": { "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" }, @@ -3107,6 +3549,9 @@ "preact@10.19.6": { "integrity": "sha512-gympg+T2Z1fG1unB8NH29yHJwnEaCH37Z32diPDku316OTnRPeMbiRV9kTrfZpocXjdfnWuFUl/Mj4BHaf6gnw==" }, + "prelude-ls@1.2.1": { + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" + }, "process-warning@4.0.1": { "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==" }, @@ -3126,6 +3571,9 @@ "ipaddr.js@1.9.1" ] }, + "punycode@2.3.1": { + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" + }, "pure-rand@6.1.0": { "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==" }, @@ -3239,6 +3687,9 @@ "requires-port@1.0.0": { "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, + "resolve-from@4.0.0": { + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + }, "resolve-pkg-maps@1.0.0": { "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==" }, @@ -3441,6 +3892,15 @@ ], "scripts": true }, + "shebang-command@2.0.0": { + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": [ + "shebang-regex" + ] + }, + "shebang-regex@3.0.0": { + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, "shiki@1.29.2": { "integrity": "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg==", "dependencies": [ @@ -3585,6 +4045,9 @@ "ansi-regex@6.2.2" ] }, + "strip-json-comments@3.1.1": { + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" + }, "strtok3@10.3.4": { "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", "dependencies": [ @@ -3704,6 +4167,12 @@ "trim-lines@3.0.1": { "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==" }, + "ts-api-utils@2.1.0_typescript@5.9.3": { + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dependencies": [ + "typescript" + ] + }, "tsdown@0.12.9_rolldown@1.0.0-beta.55": { "integrity": "sha512-MfrXm9PIlT3saovtWKf/gCJJ/NQCdE0SiREkdNC+9Qy6UHhdeDPxnkFaBD7xttVUmgp0yUHtGirpoLB+OVLuLA==", "dependencies": [ @@ -3740,6 +4209,12 @@ ], "bin": true }, + "type-check@0.4.0": { + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dependencies": [ + "prelude-ls" + ] + }, "type-fest@0.21.3": { "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==" }, @@ -3750,6 +4225,10 @@ "mime-types" ] }, + "typescript@5.9.3": { + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "bin": true + }, "ufo@1.6.1": { "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==" }, @@ -3845,6 +4324,12 @@ "unpipe@1.0.0": { "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" }, + "uri-js@4.4.1": { + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": [ + "punycode" + ] + }, "uri-template-router@1.0.0": { "integrity": "sha512-WKcL9ZSIEhHE3f5P4Z47Tf0nWbcgV1ISb/OBuF8YKEYi0SQOyTLCzM6B/gAKFWZhRhqA+C/Ks8UXe2qU5W0FVg==" }, @@ -3938,6 +4423,16 @@ "webidl-conversions" ] }, + "which@2.0.2": { + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": [ + "isexe" + ], + "bin": true + }, + "word-wrap@1.2.5": { + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==" + }, "workerd@1.20251217.0": { "integrity": "sha512-s3mHDSWwHTduyY8kpHOsl27ZJ4ziDBJlc18PfBvNMqNnhO7yBeemlxH7bo7yQyU1foJrIZ6IENHDDg0Z9N8zQA==", "optionalDependencies": [ @@ -4031,6 +4526,9 @@ "ylru@1.4.0": { "integrity": "sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==" }, + "yocto-queue@0.1.0": { + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + }, "yoctocolors-cjs@2.1.3": { "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==" }, @@ -4077,14 +4575,14 @@ "jsr:@std/testing@0.224", "jsr:@std/yaml@^1.0.8", "npm:@cloudflare/workers-types@^4.20250529.0", - "npm:@fxts/core@^1.20.0", + "npm:@fxts/core@^1.21.1", "npm:@js-temporal/polyfill@~0.5.1", "npm:@nestjs/common@^11.0.1", "npm:@opentelemetry/api@^1.9.0", "npm:@types/node@^22.16.0", "npm:amqplib@~0.10.8", "npm:byte-encodings@^1.0.11", - "npm:es-toolkit@^1.39.10", + "npm:es-toolkit@^1.42.0", "npm:h3@^1.15.0", "npm:ioredis@^5.6.1", "npm:json-preserve-indent@^1.1.3", @@ -4121,7 +4619,6 @@ "jsr:@hono/hono@^4.8.3", "jsr:@optique/core@~0.6.1", "jsr:@optique/run@~0.6.1", - "npm:@fxts/core@^1.15.0", "npm:@inquirer/prompts@^7.8.4", "npm:@jimp/core@^1.6.0", "npm:@jimp/wasm-webp@^1.6.0", @@ -4233,6 +4730,21 @@ "npm:koa@2" ] }, + "packages/lint": { + "dependencies": [ + "npm:@types/estree@^1.0.8", + "npm:eslint@9" + ], + "packageJson": { + "dependencies": [ + "npm:@types/eslint@9", + "npm:@types/estree@^1.0.8", + "npm:@typescript-eslint/parser@^8.49.0", + "npm:@typescript-eslint/utils@8", + "npm:eslint@9" + ] + } + }, "packages/relay": { "dependencies": [ "jsr:@std/assert@^1.0.13" diff --git a/packages/lint/deno.json b/packages/lint/deno.json index 5c31da9d9..1b3c8f468 100644 --- a/packages/lint/deno.json +++ b/packages/lint/deno.json @@ -10,6 +10,6 @@ "estree": "npm:@types/estree@^1.0.8" }, "tasks": { - "test": "deno test --allow-all" + "test": "deno test --allow-env" } } diff --git a/packages/lint/package.json b/packages/lint/package.json index 517b52e14..0b013b525 100644 --- a/packages/lint/package.json +++ b/packages/lint/package.json @@ -61,7 +61,7 @@ "@types/eslint": "^9.0.0", "@types/estree": "^1.0.8", "@typescript-eslint/parser": "^8.49.0", - "eslint": "catalog:", + "eslint": "^9.0.0", "tsdown": "catalog:", "typescript": "catalog:" }, diff --git a/packages/lint/src/lib/messages.ts b/packages/lint/src/lib/messages.ts index 1759794e4..73514c0ce 100644 --- a/packages/lint/src/lib/messages.ts +++ b/packages/lint/src/lib/messages.ts @@ -28,8 +28,10 @@ export const actorPropertyRequired = ({ * Generates error message for *-mismatch rules. * Used when a property exists but uses the wrong context method. * - * @param propertyName - The property name or path (e.g., "id", "endpoints.sharedInbox") - * @param expectedCall - The expected method call (e.g., "ctx.getActorUri(identifier)") + * @param propertyName - The property name or path + * (e.g., "id", "endpoints.sharedInbox") + * @param expectedCall - The expected method call + * (e.g., "ctx.getActorUri(identifier)") */ export const actorPropertyMismatch = ( context: MethodCallContext, diff --git a/packages/lint/src/lib/mismatch.ts b/packages/lint/src/lib/mismatch.ts index 6ff5f09c3..0833dbc74 100644 --- a/packages/lint/src/lib/mismatch.ts +++ b/packages/lint/src/lib/mismatch.ts @@ -144,9 +144,11 @@ function createMismatchRule( } /** - * Creates a lint rule that checks if a property uses the correct context method. + * Creates a lint rule that checks if a property uses + * the correct context method. * - * @param config Property configuration containing name, getter, setter, and nested info + * @param config Property configuration containing name, getter, setter, and + * nested info * @returns A Deno lint rule */ export const createMismatchRuleDeno = ( diff --git a/packages/lint/src/lib/pred.ts b/packages/lint/src/lib/pred.ts index f826a40c4..3960876d1 100644 --- a/packages/lint/src/lib/pred.ts +++ b/packages/lint/src/lib/pred.ts @@ -109,7 +109,8 @@ export const isFunction = ( )(expr); /** - * Checks if a CallExpression is a setActorDispatcher call with proper structure. + * Checks if a CallExpression is a setActorDispatcher call with + * proper structure. */ export const isSetActorDispatcherCall = ( node: CallExpression, diff --git a/packages/lint/src/lib/property-checker.ts b/packages/lint/src/lib/property-checker.ts index 8f6dc5e1a..ab6b0ece1 100644 --- a/packages/lint/src/lib/property-checker.ts +++ b/packages/lint/src/lib/property-checker.ts @@ -57,7 +57,8 @@ export const isPropertyWithName = (propertyName: T) => /** * Creates a predicate function that checks if a nested property exists. - * @param path Array of property names forming the path (e.g., ["endpoints", "sharedInbox"]) + * @param path Array of property names forming the path + * (e.g., ["endpoints", "sharedInbox"]) * @returns A predicate function that checks if the nested property exists */ export function createPropertyChecker( @@ -99,7 +100,8 @@ const checkObjectExpression = obj.properties.some(propertyChecker); /** - * Checks if a ConditionalExpression (ternary operator) has the property in both branches. + * Checks if a ConditionalExpression (ternary operator) has the property in + * both branches. * @param propertyChecker The predicate function to check properties * @returns A function that checks the ConditionalExpression */ diff --git a/packages/lint/src/lib/test-templates.ts b/packages/lint/src/lib/test-templates.ts index 6968fcd15..18198358a 100644 --- a/packages/lint/src/lib/test-templates.ts +++ b/packages/lint/src/lib/test-templates.ts @@ -15,9 +15,11 @@ interface TestConfig { } const createDispatcherCode = (content: string) => ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { +federation.setActorDispatcher( + "/users/{identifier}", + async (ctx, identifier) => { ${content} - }); +}); `; const createChainedDispatcherCode = ( @@ -66,7 +68,8 @@ const createMethodCall = ( * * @example * // Simple property: id: ctx.getActorUri(identifier), - * // Nested property: endpoints: new Endpoints({ sharedInbox: ctx.getInboxUri() }), + * // Nested property: + * // endpoints: new Endpoints({ sharedInbox: ctx.getInboxUri() }), */ const createPropertyAssignment = ( prop: PropertyConfig, @@ -89,7 +92,8 @@ const createPropertyAssignment = ( ); if (prop.nested) { - return `${prop.nested.parent}: new ${prop.nested.wrapper}({ ${prop.name}: ${methodCall} }),`; + return `${prop.nested.parent}: \ + new ${prop.nested.wrapper}({ ${prop.name}: ${methodCall} }),`; } return `${prop.name}: ${methodCall},`; }; @@ -483,7 +487,8 @@ export function createIdRequiredRuleTests(config: TestConfig) { } /** - * Creates required rule tests for key-related properties (publicKey, assertionMethod) + * Creates required rule tests for key-related properties + * (publicKey, assertionMethod) */ export function createKeyRequiredRuleTests( propertyName: "publicKey" | "assertionMethod", @@ -564,7 +569,8 @@ export function createKeyRequiredRuleTests( ruleName, }), - // ❌ Bad - key pairs dispatcher configured BEFORE (separate), property missing + // ❌ Bad - key pairs dispatcher configured BEFORE (separate), + // property missing "key pairs before separate property missing": lintTest({ code: createSeparateDispatcherCode( createActorWithKey(false), @@ -576,7 +582,8 @@ export function createKeyRequiredRuleTests( expectedError, }), - // ❌ Bad - key pairs dispatcher configured BEFORE (chained), property missing + // ❌ Bad - key pairs dispatcher configured BEFORE (chained), + // property missing "key pairs before chained property missing": lintTest({ code: createChainedDispatcherCode( createActorWithKey(false), @@ -587,7 +594,8 @@ export function createKeyRequiredRuleTests( expectedError, }), - // ❌ Bad - key pairs dispatcher configured AFTER (separate), property missing + // ❌ Bad - key pairs dispatcher configured AFTER (separate), + // property missing "key pairs after separate property missing": lintTest({ code: createSeparateDispatcherCode( createActorWithKey(false), @@ -599,7 +607,8 @@ export function createKeyRequiredRuleTests( expectedError, }), - // ❌ Bad - key pairs dispatcher configured AFTER (chained), property missing + // ❌ Bad - key pairs dispatcher configured AFTER (chained), + // property missing "key pairs after chained property missing": lintTest({ code: createChainedDispatcherCode( createActorWithKey(false), @@ -799,9 +808,11 @@ export function createRequiredEdgeCaseTests( // ✅ Ternary with property in both branches "ternary with property in both branches": lintTest({ code: createChainedDispatcherCode( - `return condition - ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" });`, + `\ + return condition + ? new Person({ id: ctx.getActorUri(identifier), \ + ${propCode} name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" });`, dispatcherMethod, ), rule, @@ -811,9 +822,10 @@ export function createRequiredEdgeCaseTests( // ❌ Ternary missing property in consequent "ternary missing property in consequent": lintTest({ code: createChainedDispatcherCode( - `return condition - ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" });`, + `\ + return condition + ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" });`, dispatcherMethod, ), rule, @@ -824,9 +836,10 @@ export function createRequiredEdgeCaseTests( // ❌ Ternary missing property in alternate "ternary missing property in alternate": lintTest({ code: createChainedDispatcherCode( - `return condition - ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), name: "B" });`, + `\ + return condition + ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), name: "B" });`, dispatcherMethod, ), rule, @@ -850,11 +863,12 @@ export function createRequiredEdgeCaseTests( // ✅ Nested ternary with property "nested ternary with property": lintTest({ code: createChainedDispatcherCode( - `return condition1 - ? (condition2 - ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" })) - : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" });`, + `\ +return condition1 + ? (condition2 + ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" })) + : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" });`, dispatcherMethod, ), rule, @@ -889,22 +903,14 @@ export function createMismatchEdgeCaseTests( return { // ✅ Ternary with correct getter in both branches "ternary with correct getter in both branches": lintTest({ - code: createDispatcherCode( - `return condition - ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" });`, - ), + code: createDispatcherCode(nestedTernaryCode(propCode, propCode)), rule, ruleName, }), // ❌ Ternary with wrong getter in consequent "ternary with wrong getter in consequent": lintTest({ - code: createDispatcherCode( - `return condition - ? new Person({ id: ctx.getActorUri(identifier), ${wrongPropCode} name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" });`, - ), + code: createDispatcherCode(nestedTernaryCode(wrongPropCode, propCode)), rule, ruleName, expectedError, @@ -912,11 +918,7 @@ export function createMismatchEdgeCaseTests( // ❌ Ternary with wrong getter in alternate "ternary with wrong getter in alternate": lintTest({ - code: createDispatcherCode( - `return condition - ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), ${wrongPropCode} name: "B" });`, - ), + code: createDispatcherCode(nestedTernaryCode(propCode, wrongPropCode)), rule, ruleName, expectedError, @@ -925,9 +927,7 @@ export function createMismatchEdgeCaseTests( // ❌ Ternary with wrong getter in both "ternary with wrong getter in both branches": lintTest({ code: createDispatcherCode( - `return condition - ? new Person({ id: ctx.getActorUri(identifier), ${wrongPropCode} name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), ${wrongPropCode} name: "B" });`, + nestedTernaryCode(wrongPropCode, wrongPropCode), ), rule, ruleName, @@ -937,11 +937,12 @@ export function createMismatchEdgeCaseTests( // ✅ Nested ternary with correct getter "nested ternary with correct getter": lintTest({ code: createDispatcherCode( - `return condition1 - ? (condition2 - ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" })) - : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" });`, + `\ +return condition1 + ? (condition2 + ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" })) + : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" });`, ), rule, ruleName, @@ -949,6 +950,11 @@ export function createMismatchEdgeCaseTests( }; } +const nestedTernaryCode = (prop1: string, prop2: string) => + `return condition + ? new Person({ id: ctx.getActorUri(identifier), ${prop1} name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), ${prop2} name: "B" });`; + /** * Creates edge case tests for id required rule */ @@ -1110,9 +1116,7 @@ export function createKeyRequiredEdgeCaseTests( // ✅ Ternary with property in both branches "ternary with property in both branches": lintTest({ code: createChainedDispatcherCode( - `return condition - ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" });`, + nestedTernaryCode(propCode, propCode), "setKeyPairsDispatcher", ), rule, @@ -1122,9 +1126,10 @@ export function createKeyRequiredEdgeCaseTests( // ❌ Ternary missing property in consequent "ternary missing property in consequent": lintTest({ code: createChainedDispatcherCode( - `return condition - ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" });`, + `\ + return condition + ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" });`, "setKeyPairsDispatcher", ), rule, @@ -1135,9 +1140,10 @@ export function createKeyRequiredEdgeCaseTests( // ❌ Ternary missing property in alternate "ternary missing property in alternate": lintTest({ code: createChainedDispatcherCode( - `return condition - ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), name: "B" });`, + `\ + return condition + ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), name: "B" });`, "setKeyPairsDispatcher", ), rule, @@ -1161,11 +1167,12 @@ export function createKeyRequiredEdgeCaseTests( // ✅ Nested ternary with property "nested ternary with property": lintTest({ code: createChainedDispatcherCode( - `return condition1 - ? (condition2 - ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" })) - : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" });`, + `\ +return condition1 + ? (condition2 + ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) + : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" })) + : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" });`, "setKeyPairsDispatcher", ), rule, diff --git a/packages/lint/src/rules/collection-filtering-not-implemented.ts b/packages/lint/src/rules/collection-filtering-not-implemented.ts index 77eaebf77..b47281e4c 100644 --- a/packages/lint/src/rules/collection-filtering-not-implemented.ts +++ b/packages/lint/src/rules/collection-filtering-not-implemented.ts @@ -43,12 +43,12 @@ const hasFilterParameter = hasMinParams(4); * Lint rule: collection-filtering-not-implemented * * Warns when setFollowersDispatcher doesn't implement filtering. - * The followers dispatcher should accept a 4th parameter (filter/baseUri) to support - * server-side filtering for followers collection synchronization. + * The followers dispatcher should accept a 4th parameter (filter/baseUri) to + * support server-side filtering for followers collection synchronization. * See: https://fedify.dev/manual/collections#filtering-by-server * * @example Good: - * ```ts + * ```typescript ignore * federation.setFollowersDispatcher( * "/users/{identifier}/followers", * async (ctx, identifier, cursor, filter) => { @@ -62,7 +62,7 @@ const hasFilterParameter = hasMinParams(4); * ``` * * @example Bad: - * ```ts + * ```typescript ignore * federation.setFollowersDispatcher( * "/users/{identifier}/followers", * async (ctx, identifier, cursor) => { diff --git a/packages/lint/tsdown.config.ts b/packages/lint/tsdown.config.ts index 81633e88a..c8ef53697 100644 --- a/packages/lint/tsdown.config.ts +++ b/packages/lint/tsdown.config.ts @@ -5,5 +5,4 @@ export default defineConfig({ dts: true, format: ["esm", "cjs"], platform: "node", - exports: "named", }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 490a5aa8c..aa0c95933 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,8 +64,8 @@ catalogs: specifier: ^1.3.8 version: 1.3.8 es-toolkit: - specifier: 1.39.10 - version: 1.39.10 + specifier: 1.42.0 + version: 1.42.0 express: specifier: ^4.0.0 version: 4.21.2 @@ -715,7 +715,7 @@ importers: version: 3.0.0 es-toolkit: specifier: 'catalog:' - version: 1.39.10 + version: 1.42.0 fetch-mock: specifier: 'catalog:' version: 12.6.0 @@ -968,6 +968,37 @@ importers: specifier: 'catalog:' version: 5.9.3 + packages/lint: + dependencies: + '@fedify/fedify': + specifier: workspace:^ + version: link:../fedify + '@fxts/core': + specifier: 'catalog:' + version: 1.20.0 + '@typescript-eslint/utils': + specifier: ^8.0.0 + version: 8.41.0(eslint@9.32.0(jiti@2.5.1))(typescript@5.9.3) + devDependencies: + '@types/eslint': + specifier: ^9.0.0 + version: 9.6.1 + '@types/estree': + specifier: ^1.0.8 + version: 1.0.8 + '@typescript-eslint/parser': + specifier: ^8.49.0 + version: 8.50.0(eslint@9.32.0(jiti@2.5.1))(typescript@5.9.3) + eslint: + specifier: ^9.0.0 + version: 9.32.0(jiti@2.5.1) + tsdown: + specifier: 'catalog:' + version: 0.12.9(typescript@5.9.3) + typescript: + specifier: 'catalog:' + version: 5.9.3 + packages/nestjs: dependencies: '@fedify/fedify': @@ -3869,6 +3900,9 @@ packages: '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/eslint@9.6.1': + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -4053,6 +4087,13 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/parser@8.50.0': + resolution: {integrity: sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/project-service@8.38.0': resolution: {integrity: sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4065,6 +4106,12 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/project-service@8.50.0': + resolution: {integrity: sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/scope-manager@7.18.0': resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} engines: {node: ^18.18.0 || >=20.0.0} @@ -4077,6 +4124,10 @@ packages: resolution: {integrity: sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/scope-manager@8.50.0': + resolution: {integrity: sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/tsconfig-utils@8.38.0': resolution: {integrity: sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4089,6 +4140,12 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/tsconfig-utils@8.50.0': + resolution: {integrity: sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/type-utils@7.18.0': resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==} engines: {node: ^18.18.0 || >=20.0.0} @@ -4125,6 +4182,10 @@ packages: resolution: {integrity: sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.50.0': + resolution: {integrity: sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@7.18.0': resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==} engines: {node: ^18.18.0 || >=20.0.0} @@ -4146,6 +4207,12 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/typescript-estree@8.50.0': + resolution: {integrity: sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/utils@7.18.0': resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==} engines: {node: ^18.18.0 || >=20.0.0} @@ -4178,6 +4245,10 @@ packages: resolution: {integrity: sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.50.0': + resolution: {integrity: sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript/vfs@1.6.1': resolution: {integrity: sha512-JwoxboBh7Oz1v38tPbkrZ62ZXNHAk9bJ7c9x0eI5zBfBnBYGhURdbnh7Z4smN/MV48Y5OCcZb58n972UtbazsA==} peerDependencies: @@ -5291,9 +5362,6 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - es-toolkit@1.39.10: - resolution: {integrity: sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==} - es-toolkit@1.39.5: resolution: {integrity: sha512-z9V0qU4lx1TBXDNFWfAASWk6RNU6c6+TJBKE+FLIg8u0XJ6Yw58Hi0yX8ftEouj6p1QARRlXLFfHbIli93BdQQ==} @@ -6141,10 +6209,6 @@ packages: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true - jiti@2.4.2: - resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} - hasBin: true - jiti@2.5.1: resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} hasBin: true @@ -7883,6 +7947,10 @@ packages: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + to-data-view@1.1.0: resolution: {integrity: sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ==} @@ -10777,7 +10845,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 enhanced-resolve: 5.18.2 - jiti: 2.4.2 + jiti: 2.5.1 lightningcss: 1.30.1 magic-string: 0.30.17 source-map-js: 1.2.1 @@ -11094,6 +11162,11 @@ snapshots: dependencies: '@types/ms': 2.1.0 + '@types/eslint@9.6.1': + dependencies: + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + '@types/estree@1.0.8': {} '@types/express-serve-static-core@4.19.6': @@ -11363,6 +11436,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/parser@8.50.0(eslint@9.32.0(jiti@2.5.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.50.0 + '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.50.0 + debug: 4.4.1 + eslint: 9.32.0(jiti@2.5.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/project-service@8.38.0(typescript@5.9.2)': dependencies: '@typescript-eslint/tsconfig-utils': 8.41.0(typescript@5.9.2) @@ -11390,6 +11475,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/project-service@8.41.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.41.0(typescript@5.9.3) + '@typescript-eslint/types': 8.41.0 + debug: 4.4.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.50.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3) + '@typescript-eslint/types': 8.50.0 + debug: 4.4.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/scope-manager@7.18.0': dependencies: '@typescript-eslint/types': 7.18.0 @@ -11405,6 +11508,11 @@ snapshots: '@typescript-eslint/types': 8.41.0 '@typescript-eslint/visitor-keys': 8.41.0 + '@typescript-eslint/scope-manager@8.50.0': + dependencies: + '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/visitor-keys': 8.50.0 + '@typescript-eslint/tsconfig-utils@8.38.0(typescript@5.9.2)': dependencies: typescript: 5.9.2 @@ -11421,6 +11529,10 @@ snapshots: dependencies: typescript: 5.9.3 + '@typescript-eslint/tsconfig-utils@8.50.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + '@typescript-eslint/type-utils@7.18.0(eslint@8.57.1)(typescript@5.9.3)': dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3) @@ -11475,6 +11587,8 @@ snapshots: '@typescript-eslint/types@8.41.0': {} + '@typescript-eslint/types@8.50.0': {} + '@typescript-eslint/typescript-estree@7.18.0(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 7.18.0 @@ -11538,6 +11652,37 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@8.41.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.41.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.41.0(typescript@5.9.3) + '@typescript-eslint/types': 8.41.0 + '@typescript-eslint/visitor-keys': 8.41.0 + debug: 4.4.1 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@8.50.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.50.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3) + '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/visitor-keys': 8.50.0 + debug: 4.4.1 + minimatch: 9.0.5 + semver: 7.7.2 + tinyglobby: 0.2.15 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@7.18.0(eslint@8.57.1)(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) @@ -11582,6 +11727,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.41.0(eslint@9.32.0(jiti@2.5.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.32.0(jiti@2.5.1)) + '@typescript-eslint/scope-manager': 8.41.0 + '@typescript-eslint/types': 8.41.0 + '@typescript-eslint/typescript-estree': 8.41.0(typescript@5.9.3) + eslint: 9.32.0(jiti@2.5.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/visitor-keys@7.18.0': dependencies: '@typescript-eslint/types': 7.18.0 @@ -11597,6 +11753,11 @@ snapshots: '@typescript-eslint/types': 8.41.0 eslint-visitor-keys: 4.2.1 + '@typescript-eslint/visitor-keys@8.50.0': + dependencies: + '@typescript-eslint/types': 8.50.0 + eslint-visitor-keys: 4.2.1 + '@typescript/vfs@1.6.1(typescript@5.9.3)': dependencies: debug: 4.4.1 @@ -12753,8 +12914,6 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - es-toolkit@1.39.10: {} - es-toolkit@1.39.5: {} es-toolkit@1.42.0: {} @@ -14067,8 +14226,6 @@ snapshots: jiti@1.21.7: {} - jiti@2.4.2: {} - jiti@2.5.1: {} jpeg-js@0.4.4: {} @@ -16076,6 +16233,11 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + to-data-view@1.1.0: {} to-data-view@2.0.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 05c56f271..51147c041 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -51,7 +51,7 @@ catalog: amqplib: ^0.10.8 byte-encodings: ^1.0.11 elysia: ^1.3.8 - es-toolkit: 1.39.10 + es-toolkit: 1.42.0 express: ^4.0.0 fastify: ^5.2.0 fastify-plugin: ^5.0.1 From 1c9c03934804bb73a583d6f6ee5827eaca46c005 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Mon, 15 Dec 2025 12:20:34 +0000 Subject: [PATCH 30/41] Enhanced usability for eslint plugin --- packages/lint/package.json | 3 ++- packages/lint/src/index.ts | 25 ++++++++++++++++++------- pnpm-lock.yaml | 6 +++--- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/packages/lint/package.json b/packages/lint/package.json index 0b013b525..4ac11bc38 100644 --- a/packages/lint/package.json +++ b/packages/lint/package.json @@ -42,6 +42,7 @@ }, "files": [ "dist/", + "deno.json", "package.json" ], "peerDependencies": { @@ -55,12 +56,12 @@ }, "dependencies": { "@fxts/core": "catalog:", + "@typescript-eslint/parser": "^8.49.0", "@typescript-eslint/utils": "^8.0.0" }, "devDependencies": { "@types/eslint": "^9.0.0", "@types/estree": "^1.0.8", - "@typescript-eslint/parser": "^8.49.0", "eslint": "^9.0.0", "tsdown": "catalog:", "typescript": "catalog:" diff --git a/packages/lint/src/index.ts b/packages/lint/src/index.ts index dc3358890..4671bcbc0 100644 --- a/packages/lint/src/index.ts +++ b/packages/lint/src/index.ts @@ -3,6 +3,7 @@ * Provides lint rules for validating Fedify federation code. */ import { fromEntries, keys, map, pipe } from "@fxts/core"; +import parser from "@typescript-eslint/parser"; import type { ESLint, Rule } from "eslint"; import metadata from "../deno.json" with { type: "json" }; import { RULE_IDS } from "./lib/const.ts"; @@ -124,15 +125,11 @@ const recommendedRules = pipe( const strictRules = pipe( rules, keys, - map((key) => [`@fedify/lint/${key}`, "error" as const] as const), + map((key) => [`${metadata.name as "@fedify/lint"}/${key}`, "error"] as const), fromEntries, ); -// ============================================================================ -// Plugin Export -// ============================================================================ - -const plugin = { +export const plugin = { meta: { name: metadata.name, version: metadata.version, @@ -150,4 +147,18 @@ const plugin = { }, } as const satisfies ESLint.Plugin; -export default plugin; +const recommendedConfig = { + files: ["federation", "federation/*"].map((filename) => [ + filename + ".ts", + filename + ".tsx", + filename + ".js", + filename + ".jsx", + filename + ".mjs", + filename + ".cjs", + ]).flat(), + languageOptions: { parser }, + plugins: { [metadata.name]: plugin }, + rules: recommendedRules, +}; + +export default recommendedConfig; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa0c95933..83c80c60a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -976,6 +976,9 @@ importers: '@fxts/core': specifier: 'catalog:' version: 1.20.0 + '@typescript-eslint/parser': + specifier: ^8.49.0 + version: 8.50.0(eslint@9.32.0(jiti@2.5.1))(typescript@5.9.3) '@typescript-eslint/utils': specifier: ^8.0.0 version: 8.41.0(eslint@9.32.0(jiti@2.5.1))(typescript@5.9.3) @@ -986,9 +989,6 @@ importers: '@types/estree': specifier: ^1.0.8 version: 1.0.8 - '@typescript-eslint/parser': - specifier: ^8.49.0 - version: 8.50.0(eslint@9.32.0(jiti@2.5.1))(typescript@5.9.3) eslint: specifier: ^9.0.0 version: 9.32.0(jiti@2.5.1) From 705e668ca6b6ffee92b7500c03217ac7efb04a16 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Tue, 16 Dec 2025 05:55:16 +0000 Subject: [PATCH 31/41] Applied gemini comment --- packages/lint/src/index.ts | 5 +- packages/lint/src/lib/messages.ts | 2 +- packages/lint/src/tests/integration.test.ts | 748 ++++++++++++-------- packages/lint/tsdown.config.ts | 2 +- 4 files changed, 449 insertions(+), 308 deletions(-) diff --git a/packages/lint/src/index.ts b/packages/lint/src/index.ts index 4671bcbc0..5318bb62a 100644 --- a/packages/lint/src/index.ts +++ b/packages/lint/src/index.ts @@ -110,10 +110,7 @@ const recommendedRules = pipe( map((key) => [ `@fedify/lint/${key}`, - recommendedRuleIds - .includes(key) - ? "error" as const - : "warn" as const, + recommendedRuleIds.includes(key) ? "error" : "warn", ] as const ), fromEntries, diff --git a/packages/lint/src/lib/messages.ts b/packages/lint/src/lib/messages.ts index 73514c0ce..e15f0f779 100644 --- a/packages/lint/src/lib/messages.ts +++ b/packages/lint/src/lib/messages.ts @@ -22,7 +22,7 @@ export const actorPropertyRequired = ({ path: path.join("."), requiresIdentifier, }) - }\` to retrieve key pairs.`; + }\`for the \`${path.join(".")}\` property URI.`; /** * Generates error message for *-mismatch rules. diff --git a/packages/lint/src/tests/integration.test.ts b/packages/lint/src/tests/integration.test.ts index a4f2ef12f..3675d5e63 100644 --- a/packages/lint/src/tests/integration.test.ts +++ b/packages/lint/src/tests/integration.test.ts @@ -6,11 +6,13 @@ * necessary to trigger the specific lint rule being tested. */ +import { map, pipe, prop, toArray } from "@fxts/core"; import * as parser from "@typescript-eslint/parser"; import { Linter } from "eslint"; import { equal, ok } from "node:assert/strict"; import { test } from "node:test"; -import eslintPlugin from "../index.ts"; +import { plugin as eslintPlugin } from "../index.ts"; +import { replace } from "../lib/utils.ts"; import denoPlugin from "../mod.ts"; const PLUGIN_NAME = "Deno" in globalThis ? "fedify-lint" : "@fedify/lint"; @@ -39,19 +41,14 @@ function testEslint(code: string) { const config = [{ files: ["**/*.ts"], - plugins: { - "@fedify/lint": { - meta: eslintPlugin.meta, - rules: eslintPlugin.rules, - }, - }, + plugins: { "@fedify/lint": eslintPlugin }, rules: eslintPlugin.configs.recommended.rules, languageOptions: { ecmaVersion: 2022, sourceType: "module", parser, }, - } as Linter.Config]; + }]; const results = linter.verify(code, config, "integration.test.ts"); @@ -64,35 +61,33 @@ function testEslint(code: string) { /** * Assert that the code passes all lint rules (no diagnostics). */ -function assertNoErrors(code: string, message?: string) { +function assertNoErrors(code: string) { const diagnostics = lintTest(code); equal( diagnostics.length, 0, - message ?? - `Expected no errors but got: ${ - diagnostics.map((d) => `${d.id}: ${d.message}`).join(", ") - }`, + `Expected no errors but got: ${ + diagnostics.map((d) => `${d.id}: ${d.message}`).join(", ") + }`, ); } /** * Assert that the code has exactly one error matching the given rule. */ -function assertHasError(code: string, ruleName: string, message?: string) { +const assertHasError = (ruleName: string) => (code: string) => { const diagnostics = lintTest(code); const ruleId = `${PLUGIN_NAME}/${ruleName}`; const matched = diagnostics.some((d) => d.id === ruleId); ok( matched, - message ?? - `Expected error from ${ruleName} but got: ${ - diagnostics.length === 0 - ? "no errors" - : diagnostics.map((d) => d.id).join(", ") - }`, + `Expected error from ${ruleName} but got: ${ + diagnostics.length === 0 + ? "no errors" + : diagnostics.map((d) => d.id).join(", ") + }`, ); -} +}; /** * Complete valid code that passes all lint rules. @@ -184,195 +179,314 @@ test("Integration: ✅ Complete valid code passes all rules", () => { assertNoErrors(COMPLETE_VALID_CODE); }); -test("Integration: ❌ actor-id-required - missing id property", () => { - const code = COMPLETE_VALID_CODE.replace( - "id: ctx.getActorUri(identifier),", - "// id: ctx.getActorUri(identifier), // REMOVED", - ); - assertHasError(code, "actor-id-required"); -}); - -test("Integration: ❌ actor-inbox-property-required - missing inbox property", () => { - const code = COMPLETE_VALID_CODE.replace( - "inbox: ctx.getInboxUri(identifier),", - "// inbox: ctx.getInboxUri(identifier), // REMOVED", - ); - assertHasError(code, "actor-inbox-property-required"); -}); +test("Integration: ❌ actor-id-required - missing id property", () => + pipe( + COMPLETE_VALID_CODE, + replace( + "id: ctx.getActorUri(identifier),", + "// id: ctx.getActorUri(identifier), // REMOVED", + ), + assertHasError("actor-id-required"), + )); + +test( + "Integration: ❌ actor-inbox-property-required - missing inbox property", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "inbox: ctx.getInboxUri(identifier),", + "// inbox: ctx.getInboxUri(identifier), // REMOVED", + ), + assertHasError("actor-inbox-property-required"), + ), +); -test("Integration: ❌ actor-shared-inbox-property-required - missing sharedInbox property", () => { - const code = COMPLETE_VALID_CODE.replace( - `endpoints: new Endpoints({ +test( + "Integration: ❌ actor-shared-inbox-property-required \ +- missing sharedInbox property", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + `endpoints: new Endpoints({ sharedInbox: ctx.getInboxUri(), }),`, - "// endpoints: REMOVED", - ); - assertHasError(code, "actor-shared-inbox-property-required"); -}); + "// endpoints: REMOVED", + ), + assertHasError("actor-shared-inbox-property-required"), + ), +); -test("Integration: ❌ actor-following-property-required - missing following property", () => { - const code = COMPLETE_VALID_CODE.replace( - "following: ctx.getFollowingUri(identifier),", - "// following: ctx.getFollowingUri(identifier), // REMOVED", - ); - assertHasError(code, "actor-following-property-required"); -}); +test( + "Integration: ❌ actor-following-property-required \ +- missing following property", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "following: ctx.getFollowingUri(identifier),", + "// following: ctx.getFollowingUri(identifier), // REMOVED", + ), + assertHasError("actor-following-property-required"), + ), +); -test("Integration: ❌ actor-followers-property-required - missing followers property", () => { - const code = COMPLETE_VALID_CODE.replace( - "followers: ctx.getFollowersUri(identifier),", - "// followers: ctx.getFollowersUri(identifier), // REMOVED", - ); - assertHasError(code, "actor-followers-property-required"); -}); +test( + "Integration: ❌ actor-followers-property-required \ +- missing followers property", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "followers: ctx.getFollowersUri(identifier),", + "// followers: ctx.getFollowersUri(identifier), // REMOVED", + ), + assertHasError("actor-followers-property-required"), + ), +); -test("Integration: ❌ actor-outbox-property-required - missing outbox property", () => { - const code = COMPLETE_VALID_CODE.replace( - "outbox: ctx.getOutboxUri(identifier),", - "// outbox: ctx.getOutboxUri(identifier), // REMOVED", - ); - assertHasError(code, "actor-outbox-property-required"); -}); +test( + "Integration: ❌ actor-outbox-property-required \ +- missing outbox property", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "outbox: ctx.getOutboxUri(identifier),", + "// outbox: ctx.getOutboxUri(identifier), // REMOVED", + ), + assertHasError("actor-outbox-property-required"), + ), +); -test("Integration: ❌ actor-liked-property-required - missing liked property", () => { - const code = COMPLETE_VALID_CODE.replace( - "liked: ctx.getLikedUri(identifier),", - "// liked: ctx.getLikedUri(identifier), // REMOVED", - ); - assertHasError(code, "actor-liked-property-required"); -}); +test( + "Integration: ❌ actor-liked-property-required \ +- missing liked property", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "liked: ctx.getLikedUri(identifier),", + "// liked: ctx.getLikedUri(identifier), // REMOVED", + ), + assertHasError("actor-liked-property-required"), + ), +); -test("Integration: ❌ actor-featured-property-required - missing featured property", () => { - const code = COMPLETE_VALID_CODE.replace( - "featured: ctx.getFeaturedUri(identifier),", - "// featured: ctx.getFeaturedUri(identifier), // REMOVED", - ); - assertHasError(code, "actor-featured-property-required"); -}); +test( + "Integration: ❌ actor-featured-property-required \ +- missing featured property", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "featured: ctx.getFeaturedUri(identifier),", + "// featured: ctx.getFeaturedUri(identifier), // REMOVED", + ), + assertHasError("actor-featured-property-required"), + ), +); -test("Integration: ❌ actor-featured-tags-property-required - missing featuredTags property", () => { - const code = COMPLETE_VALID_CODE.replace( - "featuredTags: ctx.getFeaturedTagsUri(identifier),", - "// featuredTags: ctx.getFeaturedTagsUri(identifier), // REMOVED", - ); - assertHasError(code, "actor-featured-tags-property-required"); -}); +test( + "Integration: ❌ actor-featured-tags-property-required \ +- missing featuredTags property", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "featuredTags: ctx.getFeaturedTagsUri(identifier),", + "// featuredTags: ctx.getFeaturedTagsUri(identifier), // REMOVED", + ), + assertHasError("actor-featured-tags-property-required"), + ), +); -test("Integration: ❌ actor-public-key-required - missing publicKey property", () => { - const code = COMPLETE_VALID_CODE.replace( - "publicKey: keyPairs[0]?.cryptographicKey,", - "// publicKey: keyPairs[0]?.cryptographicKey, // REMOVED", - ); - assertHasError(code, "actor-public-key-required"); -}); +test( + "Integration: ❌ actor-public-key-required \ +- missing publicKey property", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "publicKey: keyPairs[0]?.cryptographicKey,", + "// publicKey: keyPairs[0]?.cryptographicKey, // REMOVED", + ), + assertHasError("actor-public-key-required"), + ), +); -test("Integration: ❌ actor-assertion-method-required - missing assertionMethod property", () => { - const code = COMPLETE_VALID_CODE.replace( - "assertionMethod: keyPairs[0]?.multikey,", - "// assertionMethod: keyPairs[0]?.multikey, // REMOVED", - ); - assertHasError(code, "actor-assertion-method-required"); -}); +test( + "Integration: ❌ actor-assertion-method-required \ +- missing assertionMethod property", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "assertionMethod: keyPairs[0]?.multikey,", + "// assertionMethod: keyPairs[0]?.multikey, // REMOVED", + ), + assertHasError("actor-assertion-method-required"), + ), +); // ============================================================================= // Test: *-mismatch rules (property uses wrong context method) // ============================================================================= -test("Integration: ❌ actor-id-mismatch - id uses wrong context method", () => { - const code = COMPLETE_VALID_CODE.replace( - "id: ctx.getActorUri(identifier),", - "id: ctx.getInboxUri(identifier), // WRONG METHOD", - ); - assertHasError(code, "actor-id-mismatch"); -}); - -test("Integration: ❌ actor-inbox-property-mismatch - inbox uses wrong context method", () => { - const code = COMPLETE_VALID_CODE.replace( - "inbox: ctx.getInboxUri(identifier),", - "inbox: ctx.getOutboxUri(identifier), // WRONG METHOD", - ); - assertHasError(code, "actor-inbox-property-mismatch"); -}); +test("Integration: ❌ actor-id-mismatch - id uses wrong context method", () => + pipe( + COMPLETE_VALID_CODE, + replace( + "id: ctx.getActorUri(identifier),", + "id: ctx.getInboxUri(identifier), // WRONG METHOD", + ), + assertHasError("actor-id-mismatch"), + )); + +test( + "Integration: ❌ actor-inbox-property-mismatch \ +- inbox uses wrong context method", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "inbox: ctx.getInboxUri(identifier),", + "inbox: ctx.getOutboxUri(identifier), // WRONG METHOD", + ), + assertHasError("actor-inbox-property-mismatch"), + ), +); -test("Integration: ❌ actor-shared-inbox-property-mismatch - sharedInbox uses wrong context method", () => { - const code = COMPLETE_VALID_CODE.replace( - "sharedInbox: ctx.getInboxUri(),", - "sharedInbox: ctx.getOutboxUri(identifier), // WRONG METHOD", - ); - assertHasError(code, "actor-shared-inbox-property-mismatch"); -}); +test( + "Integration: ❌ actor-shared-inbox-property-mismatch \ +- sharedInbox uses wrong context method", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "sharedInbox: ctx.getInboxUri(),", + "sharedInbox: ctx.getOutboxUri(identifier), // WRONG METHOD", + ), + assertHasError("actor-shared-inbox-property-mismatch"), + ), +); -test("Integration: ❌ actor-following-property-mismatch - following uses wrong context method", () => { - const code = COMPLETE_VALID_CODE.replace( - "following: ctx.getFollowingUri(identifier),", - "following: ctx.getFollowersUri(identifier), // WRONG METHOD", - ); - assertHasError(code, "actor-following-property-mismatch"); -}); +test( + "Integration: ❌ actor-following-property-mismatch \ +- following uses wrong context method", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "following: ctx.getFollowingUri(identifier),", + "following: ctx.getFollowersUri(identifier), // WRONG METHOD", + ), + assertHasError("actor-following-property-mismatch"), + ), +); -test("Integration: ❌ actor-followers-property-mismatch - followers uses wrong context method", () => { - const code = COMPLETE_VALID_CODE.replace( - "followers: ctx.getFollowersUri(identifier),", - "followers: ctx.getFollowingUri(identifier), // WRONG METHOD", - ); - assertHasError(code, "actor-followers-property-mismatch"); -}); +test( + "Integration: ❌ actor-followers-property-mismatch \ +- followers uses wrong context method", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "followers: ctx.getFollowersUri(identifier),", + "followers: ctx.getFollowingUri(identifier), // WRONG METHOD", + ), + assertHasError("actor-followers-property-mismatch"), + ), +); -test("Integration: ❌ actor-outbox-property-mismatch - outbox uses wrong context method", () => { - const code = COMPLETE_VALID_CODE.replace( - "outbox: ctx.getOutboxUri(identifier),", - "outbox: ctx.getInboxUri(identifier), // WRONG METHOD", - ); - assertHasError(code, "actor-outbox-property-mismatch"); -}); +test( + "Integration: ❌ actor-outbox-property-mismatch \ +- outbox uses wrong context method", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "outbox: ctx.getOutboxUri(identifier),", + "outbox: ctx.getInboxUri(identifier), // WRONG METHOD", + ), + assertHasError("actor-outbox-property-mismatch"), + ), +); -test("Integration: ❌ actor-liked-property-mismatch - liked uses wrong context method", () => { - const code = COMPLETE_VALID_CODE.replace( - "liked: ctx.getLikedUri(identifier),", - "liked: ctx.getOutboxUri(identifier), // WRONG METHOD", - ); - assertHasError(code, "actor-liked-property-mismatch"); -}); +test( + "Integration: ❌ actor-liked-property-mismatch \ +- liked uses wrong context method", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "liked: ctx.getLikedUri(identifier),", + "liked: ctx.getOutboxUri(identifier), // WRONG METHOD", + ), + assertHasError("actor-liked-property-mismatch"), + ), +); -test("Integration: ❌ actor-featured-property-mismatch - featured uses wrong context method", () => { - const code = COMPLETE_VALID_CODE.replace( - "featured: ctx.getFeaturedUri(identifier),", - "featured: ctx.getOutboxUri(identifier), // WRONG METHOD", - ); - assertHasError(code, "actor-featured-property-mismatch"); -}); +test( + "Integration: ❌ actor-featured-property-mismatch \ +- featured uses wrong context method", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "featured: ctx.getFeaturedUri(identifier),", + "featured: ctx.getOutboxUri(identifier), // WRONG METHOD", + ), + assertHasError("actor-featured-property-mismatch"), + ), +); -test("Integration: ❌ actor-featured-tags-property-mismatch - featuredTags uses wrong context method", () => { - const code = COMPLETE_VALID_CODE.replace( - "featuredTags: ctx.getFeaturedTagsUri(identifier),", - "featuredTags: ctx.getOutboxUri(identifier), // WRONG METHOD", - ); - assertHasError(code, "actor-featured-tags-property-mismatch"); -}); +test( + "Integration: ❌ actor-featured-tags-property-mismatch \ +- featuredTags uses wrong context method", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "featuredTags: ctx.getFeaturedTagsUri(identifier),", + "featuredTags: ctx.getOutboxUri(identifier), // WRONG METHOD", + ), + assertHasError("actor-featured-tags-property-mismatch"), + ), +); // ============================================================================= // Test: collection-filtering-not-implemented // ============================================================================= -test("Integration: ❌ collection-filtering-not-implemented - setFollowersDispatcher without filter parameter", () => { - const code = COMPLETE_VALID_CODE.replace( - "async (_ctx, _identifier, _cursor, _filter) => {", - "async (_ctx, _identifier, _cursor) => { // NO FILTER", - ); - assertHasError(code, "collection-filtering-not-implemented"); -}); +test( + "Integration: ❌ collection-filtering-not-implemented \ +- setFollowersDispatcher without filter parameter", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + "async (_ctx, _identifier, _cursor, _filter) => {", + "async (_ctx, _identifier, _cursor) => { // NO FILTER", + ), + assertHasError("collection-filtering-not-implemented"), + ), +); // ============================================================================= // Test: Non-Federation objects should not trigger errors // ============================================================================= -test("Integration: ✅ Non-Federation object - custom federation object", () => { - const code = COMPLETE_VALID_CODE.replace( - `const federation = createFederation({ +test("Integration: ✅ Non-Federation object - custom federation object", () => + pipe( + COMPLETE_VALID_CODE, + replace( + `const federation = createFederation({ kv: new MemoryKvStore(), queue: new InProcessMessageQueue(), });`, - `const federation = { + `const federation = { setActorDispatcher: () => ({ setKeyPairsDispatcher: () => {} }), setInboxListeners: () => {}, setOutboxDispatcher: () => {}, @@ -382,26 +496,28 @@ test("Integration: ✅ Non-Federation object - custom federation object", () => setFeaturedDispatcher: () => {}, setFeaturedTagsDispatcher: () => {}, };`, - ); - assertNoErrors(code); -}); + ), + assertNoErrors, + )); // ============================================================================= // Test: Dispatcher not configured - property not required // ============================================================================= -test("Integration: ✅ No setFollowingDispatcher - following property not required", () => { - // Remove the following property AND the setFollowingDispatcher - const code = COMPLETE_VALID_CODE - .replace( - ` outbox: ctx.getOutboxUri(identifier), +test( + "Integration: ✅ No setFollowingDispatcher - following property not required", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + ` outbox: ctx.getOutboxUri(identifier), following: ctx.getFollowingUri(identifier), followers: ctx.getFollowersUri(identifier),`, - ` outbox: ctx.getOutboxUri(identifier), + ` outbox: ctx.getOutboxUri(identifier), followers: ctx.getFollowersUri(identifier),`, - ) - .replace( - `federation.setFollowingDispatcher( + ), + replace( + `federation.setFollowingDispatcher( "/users/{identifier}/following", async (_ctx, _identifier, _cursor) => { return { items: [] }; @@ -409,23 +525,26 @@ test("Integration: ✅ No setFollowingDispatcher - following property not requir ); `, - "", - ); - - assertNoErrors(code); -}); + "", + ), + assertNoErrors, + ), +); -test("Integration: ✅ No setFollowersDispatcher - followers property not required", () => { - const code = COMPLETE_VALID_CODE - .replace( - ` following: ctx.getFollowingUri(identifier), +test( + "Integration: ✅ No setFollowersDispatcher - followers property not required", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + ` following: ctx.getFollowingUri(identifier), followers: ctx.getFollowersUri(identifier), liked: ctx.getLikedUri(identifier),`, - ` following: ctx.getFollowingUri(identifier), + ` following: ctx.getFollowingUri(identifier), liked: ctx.getLikedUri(identifier),`, - ) - .replace( - `federation.setFollowersDispatcher( + ), + replace( + `federation.setFollowersDispatcher( "/users/{identifier}/followers", async (_ctx, _identifier, _cursor, _filter) => { return { items: [] }; @@ -433,22 +552,26 @@ test("Integration: ✅ No setFollowersDispatcher - followers property not requir ); `, - "", - ); - assertNoErrors(code); -}); + "", + ), + assertNoErrors, + ), +); -test("Integration: ✅ No setOutboxDispatcher - outbox property not required", () => { - const code = COMPLETE_VALID_CODE - .replace( - ` }), +test( + "Integration: ✅ No setOutboxDispatcher - outbox property not required", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + ` }), outbox: ctx.getOutboxUri(identifier), following: ctx.getFollowingUri(identifier),`, - ` }), + ` }), following: ctx.getFollowingUri(identifier),`, - ) - .replace( - `federation.setOutboxDispatcher( + ), + replace( + `federation.setOutboxDispatcher( "/users/{identifier}/outbox", async (_ctx, _identifier, _cursor) => { return { items: [] }; @@ -456,22 +579,26 @@ test("Integration: ✅ No setOutboxDispatcher - outbox property not required", ( ); `, - "", - ); - assertNoErrors(code); -}); + "", + ), + assertNoErrors, + ), +); -test("Integration: ✅ No setLikedDispatcher - liked property not required", () => { - const code = COMPLETE_VALID_CODE - .replace( - ` followers: ctx.getFollowersUri(identifier), +test( + "Integration: ✅ No setLikedDispatcher - liked property not required", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + ` followers: ctx.getFollowersUri(identifier), liked: ctx.getLikedUri(identifier), featured: ctx.getFeaturedUri(identifier),`, - ` followers: ctx.getFollowersUri(identifier), + ` followers: ctx.getFollowersUri(identifier), featured: ctx.getFeaturedUri(identifier),`, - ) - .replace( - `federation.setLikedDispatcher( + ), + replace( + `federation.setLikedDispatcher( "/users/{identifier}/liked", async (_ctx, _identifier, _cursor) => { return { items: [] }; @@ -479,22 +606,26 @@ test("Integration: ✅ No setLikedDispatcher - liked property not required", () ); `, - "", - ); - assertNoErrors(code); -}); + "", + ), + assertNoErrors, + ), +); -test("Integration: ✅ No setFeaturedDispatcher - featured property not required", () => { - const code = COMPLETE_VALID_CODE - .replace( - ` liked: ctx.getLikedUri(identifier), +test( + "Integration: ✅ No setFeaturedDispatcher - featured property not required", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + ` liked: ctx.getLikedUri(identifier), featured: ctx.getFeaturedUri(identifier), featuredTags: ctx.getFeaturedTagsUri(identifier),`, - ` liked: ctx.getLikedUri(identifier), + ` liked: ctx.getLikedUri(identifier), featuredTags: ctx.getFeaturedTagsUri(identifier),`, - ) - .replace( - `federation.setFeaturedDispatcher( + ), + replace( + `federation.setFeaturedDispatcher( "/users/{identifier}/featured", async (_ctx, _identifier, _cursor) => { return { items: [] }; @@ -502,107 +633,120 @@ test("Integration: ✅ No setFeaturedDispatcher - featured property not required ); `, - "", - ); - assertNoErrors(code); -}); + "", + ), + assertNoErrors, + ), +); -test("Integration: ✅ No setFeaturedTagsDispatcher - featuredTags property not required", () => { - const code = COMPLETE_VALID_CODE - .replace( - ` featured: ctx.getFeaturedUri(identifier), +test( + "Integration: ✅ No setFeaturedTagsDispatcher \ +- featuredTags property not required", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + ` featured: ctx.getFeaturedUri(identifier), featuredTags: ctx.getFeaturedTagsUri(identifier), publicKey: keyPairs[0]?.cryptographicKey,`, - ` featured: ctx.getFeaturedUri(identifier), + ` featured: ctx.getFeaturedUri(identifier), publicKey: keyPairs[0]?.cryptographicKey,`, - ) - .replace( - `federation.setFeaturedTagsDispatcher( + ), + replace( + `federation.setFeaturedTagsDispatcher( "/users/{identifier}/tags", async (_ctx, _identifier, _cursor) => { return { items: [] }; }, ); `, - "", - ); - assertNoErrors(code); -}); + "", + ), + assertNoErrors, + ), +); -test("Integration: ✅ No setInboxListeners - inbox/sharedInbox not required", () => { - const code = COMPLETE_VALID_CODE - .replace( - ` summary: "A test actor for comprehensive lint rule validation", +test( + "Integration: ✅ No setInboxListeners - inbox/sharedInbox not required", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + ` summary: "A test actor for comprehensive lint rule validation", inbox: ctx.getInboxUri(identifier), endpoints: new Endpoints({ sharedInbox: ctx.getInboxUri(), }), outbox: ctx.getOutboxUri(identifier),`, - ` summary: "A test actor for comprehensive lint rule validation", + ` summary: "A test actor for comprehensive lint rule validation", outbox: ctx.getOutboxUri(identifier),`, - ) - .replace( - `federation.setInboxListeners("/users/{identifier}/inbox", "/inbox"); + ), + replace( + `federation.setInboxListeners("/users/{identifier}/inbox", "/inbox"); `, - "", - ); - assertNoErrors(code); -}); + "", + ), + assertNoErrors, + ), +); -test("Integration: ✅ No setKeyPairsDispatcher - publicKey/assertionMethod not required", () => { - const code = COMPLETE_VALID_CODE - .replace( - ` .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { +test( + "Integration: ✅ No setKeyPairsDispatcher \ +- publicKey/assertionMethod not required", + () => + pipe( + COMPLETE_VALID_CODE, + replace( + ` .setActorDispatcher("/users/{identifier}", \ +async (ctx, identifier) => { const keyPairs = await ctx.getActorKeyPairs(identifier); return new Person({`, - ` .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + ` .setActorDispatcher("/users/{identifier}", \ +async (ctx, identifier) => { return new Person({`, - ) - .replace( - ` featuredTags: ctx.getFeaturedTagsUri(identifier), + ), + replace( + ` featuredTags: ctx.getFeaturedTagsUri(identifier), publicKey: keyPairs[0]?.cryptographicKey, assertionMethod: keyPairs[0]?.multikey, });`, - ` featuredTags: ctx.getFeaturedTagsUri(identifier), + ` featuredTags: ctx.getFeaturedTagsUri(identifier), });`, - ) - .replace( - ` }) + ), + replace( + ` }) .setKeyPairsDispatcher(async (_ctx, _identifier) => []); federation.setInboxListeners`, - ` }); + ` }); federation.setInboxListeners`, - ); - assertNoErrors(code); -}); + ), + assertNoErrors, + ), +); // ============================================================================= // Test: Multiple errors in one file // ============================================================================= -test("Integration: ❌ Multiple errors - missing id and inbox", () => { - const code = COMPLETE_VALID_CODE - .replace( - "id: ctx.getActorUri(identifier),", - "// id: REMOVED", - ) - .replace( - "inbox: ctx.getInboxUri(identifier),", - "// inbox: REMOVED", - ); - - const diagnostics = lintTest(code); - const ruleIds = diagnostics.map((d) => d.id); - - ok( - ruleIds.includes(`${PLUGIN_NAME}/actor-id-required`), - "Expected actor-id-required error", - ); - ok( - ruleIds.includes(`${PLUGIN_NAME}/actor-inbox-property-required`), - "Expected actor-inbox-property-required error", - ); -}); +test("Integration: ❌ Multiple errors - missing id and inbox", () => + pipe( + COMPLETE_VALID_CODE, + replace("id: ctx.getActorUri(identifier),", "// id: REMOVED"), + replace("inbox: ctx.getInboxUri(identifier),", "// inbox: REMOVED"), + lintTest, + map(prop("id")), + toArray, + (ids) => + [ + "actor-id-required", + "actor-inbox-property-required", + ].forEach((ruleName) => + ok( + ids.includes(`${PLUGIN_NAME}/${ruleName}`), + `Expected ${ruleName} error`, + ) + ), + )); diff --git a/packages/lint/tsdown.config.ts b/packages/lint/tsdown.config.ts index c8ef53697..70a57795c 100644 --- a/packages/lint/tsdown.config.ts +++ b/packages/lint/tsdown.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from "tsdown"; export default defineConfig({ - entry: ["src/index.ts", "src/eslint.ts"], + entry: ["src/index.ts"], dts: true, format: ["esm", "cjs"], platform: "node", From 0d9f55b4f93e45a33f71f126fa54fa9ef3e24198 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Tue, 16 Dec 2025 08:57:47 +0000 Subject: [PATCH 32/41] Added logic to check if statement --- packages/lint/src/lib/pred.ts | 5 +- packages/lint/src/lib/property-checker.ts | 189 ++++++++++++++-------- packages/lint/src/lib/utils.ts | 44 ++--- 3 files changed, 152 insertions(+), 86 deletions(-) diff --git a/packages/lint/src/lib/pred.ts b/packages/lint/src/lib/pred.ts index 3960876d1..5ea3a97d6 100644 --- a/packages/lint/src/lib/pred.ts +++ b/packages/lint/src/lib/pred.ts @@ -1,4 +1,4 @@ -import { pipe, prop } from "@fxts/core"; +import { isObject, pipe, prop } from "@fxts/core"; import type { CallExpression, CallMemberExpression, @@ -33,6 +33,9 @@ export const anyOf = (...predicates: ((value: T) => boolean)[]) => (value: T): boolean => predicates.some((predicate) => predicate(value)); +export const isNode = (obj: unknown): obj is Node => + isObject(obj) && "type" in obj; + /** * Checks if a node is of a specific type. */ diff --git a/packages/lint/src/lib/property-checker.ts b/packages/lint/src/lib/property-checker.ts index ab6b0ece1..1c00f890b 100644 --- a/packages/lint/src/lib/property-checker.ts +++ b/packages/lint/src/lib/property-checker.ts @@ -1,16 +1,17 @@ import { always, + every, head, isEmpty, - isNil, isObject, pipe, pipeLazy, prop, + toArray, unless, when, } from "@fxts/core"; -import { allOf, isNodeType } from "./pred.ts"; +import { allOf, isNode, isNodeType } from "./pred.ts"; import type { AssignmentPattern, BlockStatement, @@ -26,7 +27,7 @@ import type { Statement, WithIdentifierKey, } from "./types.ts"; -import { eq } from "./utils.ts"; +import { cases, eq } from "./utils.ts"; /** * Checks if a node has a key with a specific name. @@ -105,32 +106,29 @@ const checkObjectExpression = * @param propertyChecker The predicate function to check properties * @returns A function that checks the ConditionalExpression */ -const checkConditionalExpression = ( - propertyChecker: PropertyChecker, -) => -(node: ConditionalExpression): boolean => - [node.consequent, node.alternate].every(checkBranchWith(propertyChecker)); +const checkConditionalExpression = + (propertyChecker: PropertyChecker) => + (node: ConditionalExpression): boolean => + [node.consequent, node.alternate].every(checkBranchWith(propertyChecker)); // Check if both branches have the property const checkBranchWith = - (propertyChecker: PropertyChecker) => (branch: Expression): boolean => { - // Handle nested ternary operators - if (isNodeType("ConditionalExpression")(branch)) { - return checkConditionalExpression(propertyChecker)(branch); - } - const objExpr = extractObjectExpression(branch); - return objExpr ? checkObjectExpression(propertyChecker)(objExpr) : false; - }; - -/** - * Extracts ObjectExpression from NewExpression. - */ -const extractObjectExpression = (arg: Expression): ObjectExpression | null => { - if (isNodeType("NewExpression")(arg)) { - return extractFirstObjectExpression(arg); - } - return null; -}; + (propertyChecker: PropertyChecker) => (branch: Expression): boolean => + pipe( + branch, + cases( + isNodeType("ConditionalExpression"), + checkConditionalExpression(propertyChecker), + pipeLazy( + extractObjectExpression, + cases( + isObject, + checkObjectExpression(propertyChecker), + always(false) as (_: null) => boolean, + ), + ), + ) as (node: Expression) => boolean, + ); /** * Extracts the first argument if it's an ObjectExpression. @@ -148,55 +146,116 @@ const extractFirstObjectExpression = (node: NewExpression): ) as () => ObjectExpression | null, ); +/** + * Extracts ObjectExpression from NewExpression. + */ +const extractObjectExpression: (arg: Expression) => ObjectExpression | null = + cases( + isNodeType("NewExpression"), + extractFirstObjectExpression, + always(null), + ) as (arg: Expression) => ObjectExpression | null; + /** * Checks if a ReturnStatement node contains a property. * @param propertyChecker The predicate function to check properties * @returns A function that checks the ReturnStatement */ -export const checkReturnStatement = ( - propertyChecker: PropertyChecker, -) => -(node: ReturnStatement) => { - const arg = node.argument; - if (isNil(arg)) return false; - - // Handle ConditionalExpression (ternary operator) - if (isNodeType("ConditionalExpression")(arg)) { - return checkConditionalExpression(propertyChecker)(arg); - } - - const objExpr = extractObjectExpression(arg); - return objExpr ? checkObjectExpression(propertyChecker)(objExpr) : false; -}; +const checkReturnStatement = + (propertyChecker: PropertyChecker) => (node: ReturnStatement) => + pipe( + node, + prop("argument"), + cases( + isObject, + checkBranchWith(propertyChecker), + always(false), + ), + ); /** * Creates a function that recursively checks for a property in an AST node. * @param propertyChecker The predicate function to check properties * @returns A recursive function that checks the AST node */ -export const createPropertySearcher = (propertyChecker: PropertyChecker) => -( - node: - | Expression +export const createPropertySearcher = (propertyChecker: PropertyChecker) => { + return ( + node: Expression | BlockStatement | Statement, + ): node is + | ReturnStatement | BlockStatement - | Statement, -): node is - | ReturnStatement - | BlockStatement - | NewExpression => { - switch (node.type) { - case "ReturnStatement": - return checkReturnStatement(propertyChecker)(node); - case "BlockStatement": - return node.body.some(createPropertySearcher(propertyChecker)); - case "NewExpression": - return pipe( - node, - extractFirstObjectExpression, - when(isObject, checkObjectExpression(propertyChecker)), - Boolean, - ); - default: - return false; - } + | NewExpression => { + switch (node.type) { + case "ReturnStatement": + return checkReturnStatement(propertyChecker)(node); + + case "BlockStatement": + return checkAllReturnPaths(propertyChecker)(node); + + case "NewExpression": + return pipe( + node, + extractFirstObjectExpression, + when(isObject, checkObjectExpression(propertyChecker)), + Boolean, + ); + + default: + return false; + } + }; }; + +const checkAllReturnPaths = (propertyChecker: PropertyChecker) => +( + node: Expression | BlockStatement | Statement, +): boolean => + pipe( + node, + collectReturnPaths, + cases( + isEmpty, + always(false), + every(checkReturnStatement(propertyChecker)), + ), + ); + +/** + * Collects all return statements from a node, traversing control flow. + * This handles if/else branches, loops, etc. + */ +const collectReturnPaths = ( + node: Expression | BlockStatement | Statement, +): ReturnStatement[] => + pipe( + node, + flatten, + toArray, + ); + +function* flatten(node: Node): Generator { + if (isNodeType("ReturnStatement")(node)) yield node; + + if (isNodeType("IfStatement")(node)) { + // Collect returns from both branches + if (node.consequent) yield* flatten(node.consequent); + if (node.alternate) yield* flatten(node.alternate); + } + + if (isNodeType("BlockStatement")(node)) { + yield* node.body.map(flatten).flatMap(toArrayIfIter); + } + + for (const child of Object.values(node)) { + if (isNode(child)) { + yield* flatten(child); + } else if (Array.isArray(child)) { + yield* child.filter(isNode).map(flatten).flatMap(toArrayIfIter); + } + } +} + +const toArrayIfIter = (input: T | Iterable): T[] => + Symbol.iterator in Object(input) + ? toArray(input as Iterable) + : [input as T]; diff --git a/packages/lint/src/lib/utils.ts b/packages/lint/src/lib/utils.ts index 447404cdb..cda9ea40a 100644 --- a/packages/lint/src/lib/utils.ts +++ b/packages/lint/src/lib/utils.ts @@ -31,26 +31,7 @@ export const getArticle = (word: string): string => export const endsWith = (suffix: string) => (str: string): boolean => str.endsWith(suffix); -/* -export function replace(searchValue: string | RegExp, replaceValue: string): ( - str: string, -) => string; -export function replace( - searchValue: string | RegExp, - replacer: (substring: string, ...args: unknown[]) => string, -): ( - str: string, -) => string; -export function replace( - searchValue: string | RegExp, - replaceValue: string | ((substring: string, ...args: unknown[]) => string), -): (str: string) => string { - return (str: string) => - typeof replaceValue === "function" - ? str.replace(searchValue, replaceValue) - : str.replace(searchValue, replaceValue); -} - */ + export const replace: { (searchValue: string | RegExp, replaceValue: string): (str: string) => string; ( @@ -67,3 +48,26 @@ export const replace: { // @ts-ignore tsc cannot infer the type here replaceValue, ); + +export function cases( + pred: (value: T) => value is T1 & T, + ifTrue: (value: T1) => R1, + ifFalse: (value: T2) => R2, +): (value: T) => R1 | R2; +export function cases( + pred: (value: T) => value is T1 & T, + ifTrue: (value: T1) => R, + ifFalse: (value: T2) => R, +): (value: unknown) => R | R; +export function cases( + pred: (value: T) => boolean, + ifTrue: (value: T) => R, + ifFalse: (value: T) => R, +): (value: T) => R; +export function cases( + pred: (value: T) => boolean, + ifTrue: (value: T) => R, + ifFalse: (value: T) => R, +): (value: T) => R { + return (value: T): R => pred(value) ? ifTrue(value) : ifFalse(value); +} From 88315e429d7b7c24d160d79946365b2df993e360 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Tue, 16 Dec 2025 08:58:28 +0000 Subject: [PATCH 33/41] Added test to check if statement --- packages/lint/src/lib/test-templates.ts | 747 +++++++++++++++++- .../actor-assertion-method-required.test.ts | 61 ++ .../actor-featured-property-mismatch.test.ts | 36 + .../actor-featured-property-required.test.ts | 36 + ...or-featured-tags-property-mismatch.test.ts | 36 + ...or-featured-tags-property-required.test.ts | 36 + .../actor-followers-property-mismatch.test.ts | 36 + .../actor-followers-property-required.test.ts | 36 + .../actor-following-property-mismatch.test.ts | 36 + .../actor-following-property-required.test.ts | 36 + .../lint/src/tests/actor-id-mismatch.test.ts | 252 ++---- .../lint/src/tests/actor-id-required.test.ts | 362 ++------- .../actor-inbox-property-mismatch.test.ts | 36 + .../actor-inbox-property-required.test.ts | 36 + .../actor-liked-property-mismatch.test.ts | 36 + .../actor-liked-property-required.test.ts | 36 + .../actor-outbox-property-mismatch.test.ts | 36 + .../actor-outbox-property-required.test.ts | 36 + .../tests/actor-public-key-required.test.ts | 61 ++ ...tor-shared-inbox-property-mismatch.test.ts | 36 + ...tor-shared-inbox-property-required.test.ts | 36 + 21 files changed, 1551 insertions(+), 508 deletions(-) diff --git a/packages/lint/src/lib/test-templates.ts b/packages/lint/src/lib/test-templates.ts index 18198358a..9682c7a7a 100644 --- a/packages/lint/src/lib/test-templates.ts +++ b/packages/lint/src/lib/test-templates.ts @@ -735,22 +735,24 @@ export function createIdMismatchRuleTests(config: TestConfig) { ruleName, }), - // ✅ Good - literal string id + // ❌ Bad - literal string id "literal string id": lintTest({ code: createDispatcherCode( `return new Person({ id: "https://example.com/users/123" });`, ), rule, ruleName, + expectedError, }), - // ✅ Good - new URL as id + // ❌ Bad - new URL as id "new URL as id": lintTest({ code: createDispatcherCode( `return new Person({ id: new URL("https://example.com/users/123") });`, ), rule, ruleName, + expectedError, }), // ❌ Bad - wrong getter used @@ -763,16 +765,6 @@ export function createIdMismatchRuleTests(config: TestConfig) { expectedError, }), - // ❌ Bad - wrong method in object literal - "wrong method in object literal": lintTest({ - code: createDispatcherCode( - `return { id: ctx.getInboxUri(identifier) };`, - ), - rule, - ruleName, - expectedError, - }), - // ❌ Bad - wrong identifier "wrong identifier": lintTest({ code: createDispatcherCode( @@ -874,6 +866,156 @@ return condition1 rule, ruleName, }), + + // ✅ If/else with property in both branches + "if else with property in both branches": lintTest({ + code: createChainedDispatcherCode( + `\ +if (condition) { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); +} else { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); +}`, + dispatcherMethod, + ), + rule, + ruleName, + }), + + // ❌ If/else missing property in if block + "if else missing property in if block": lintTest({ + code: createChainedDispatcherCode( + `\ +if (condition) { + return new Person({ id: ctx.getActorUri(identifier), name: "A" }); +} else { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); +}`, + dispatcherMethod, + ), + rule, + ruleName, + expectedError, + }), + + // ❌ If/else missing property in else block + "if else missing property in else block": lintTest({ + code: createChainedDispatcherCode( + `\ +if (condition) { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); +} else { + return new Person({ id: ctx.getActorUri(identifier), name: "B" }); +}`, + dispatcherMethod, + ), + rule, + ruleName, + expectedError, + }), + + // ❌ If/else missing property in both blocks + "if else missing property in both blocks": lintTest({ + code: createChainedDispatcherCode( + `\ +if (condition) { + return new Person({ id: ctx.getActorUri(identifier), name: "A" }); +} else { + return new Person({ id: ctx.getActorUri(identifier), name: "B" }); +}`, + dispatcherMethod, + ), + rule, + ruleName, + expectedError, + }), + + // ✅ Nested if with property + "nested if with property": lintTest({ + code: createChainedDispatcherCode( + `\ +if (condition1) { + if (condition2) { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); + } else { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); + } +} else { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" }); +}`, + dispatcherMethod, + ), + rule, + ruleName, + }), + + // ✅ If/else if/else with property in all branches + "if else if else with property in all branches": lintTest({ + code: createChainedDispatcherCode( + `\ +if (condition1) { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); +} else if (condition2) { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); +} else { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" }); +}`, + dispatcherMethod, + ), + rule, + ruleName, + }), + + // ❌ If/else if/else missing property in else if + "if else if else missing property in else if": lintTest({ + code: createChainedDispatcherCode( + `\ +if (condition1) { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); +} else if (condition2) { + return new Person({ id: ctx.getActorUri(identifier), name: "B" }); +} else { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" }); +}`, + dispatcherMethod, + ), + rule, + ruleName, + expectedError, + }), + + // ✅ If/else if with final return, property in all paths + "if else if with final return property in all paths": lintTest({ + code: createChainedDispatcherCode( + `\ +if (condition1) { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); +} else if (condition2) { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); +} +return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" });`, + dispatcherMethod, + ), + rule, + ruleName, + }), + + // ❌ If/else if with final return, missing property in final return + "if else if with final return missing property in final return": lintTest({ + code: createChainedDispatcherCode( + `\ +if (condition1) { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); +} else if (condition2) { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); +} +return new Person({ id: ctx.getActorUri(identifier), name: "C" });`, + dispatcherMethod, + ), + rule, + ruleName, + expectedError, + }), }; } @@ -947,6 +1089,155 @@ return condition1 rule, ruleName, }), + + // ✅ If/else with correct getter in both branches + "if else with correct getter in both branches": lintTest({ + code: createDispatcherCode( + `\ +if (condition) { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); +} else { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); +}`, + ), + rule, + ruleName, + }), + + // ❌ If/else with wrong getter in if block + "if else with wrong getter in if block": lintTest({ + code: createDispatcherCode( + `\ +if (condition) { + return new Person({ id: ctx.getActorUri(identifier), \ + ${wrongPropCode} name: "A" }); +} else { + return new Person({ id: ctx.getActorUri(identifier), \ +${propCode} name: "B" }); +}`, + ), + rule, + ruleName, + expectedError, + }), + + // ❌ If/else with wrong getter in else block + "if else with wrong getter in else block": lintTest({ + code: createDispatcherCode( + `\ +if (condition) { + return new Person({ id: ctx.getActorUri(identifier), \ +${propCode} name: "A" }); +} else { + return new Person({ id: ctx.getActorUri(identifier), \ +${wrongPropCode} name: "B" }); +}`, + ), + rule, + ruleName, + expectedError, + }), + + // ❌ If/else with wrong getter in both blocks + "if else with wrong getter in both blocks": lintTest({ + code: createDispatcherCode( + `\ +if (condition) { + return new Person({ id: ctx.getActorUri(identifier), \ +${wrongPropCode} name: "A" }); +} else { + return new Person({ id: ctx.getActorUri(identifier), \ +${wrongPropCode} name: "B" }); +}`, + ), + rule, + ruleName, + expectedError, + }), + + // ✅ Nested if with correct getter + "nested if with correct getter": lintTest({ + code: createDispatcherCode( + `\ +if (condition1) { + if (condition2) { + return new Person({ id: ctx.getActorUri(identifier), \ + ${propCode} name: "A" }); + } else { + return new Person({ id: ctx.getActorUri(identifier), \ + ${propCode} name: "B" }); + } +} else { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" }); +}`, + ), + rule, + ruleName, + }), + + // ✅ If/else if/else with correct getter in all branches + "if else if else with correct getter in all branches": lintTest({ + code: createDispatcherCode( + `\ +if (condition1) { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); +} else if (condition2) { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); +} else { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" }); +}`, + ), + rule, + ruleName, + }), + + // ❌ If/else if/else with wrong getter in else if + "if else if else with wrong getter in else if": lintTest({ + code: createDispatcherCode( + `\ +if (condition1) { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); +} else if (condition2) { + return new Person({ id: ctx.getActorUri(identifier), ${wrongPropCode} name: "B" }); +} else { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" }); +}`, + ), + rule, + ruleName, + expectedError, + }), + + // ✅ If/else if with final return, correct getter in all paths + "if else if with final return correct getter in all paths": lintTest({ + code: createDispatcherCode( + `\ +if (condition1) { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); +} else if (condition2) { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); +} +return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" });`, + ), + rule, + ruleName, + }), + + // ❌ If/else if with final return, wrong getter in final return + "if else if with final return wrong getter in final return": lintTest({ + code: createDispatcherCode( + `\ +if (condition1) { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); +} else if (condition2) { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); +} +return new Person({ id: ctx.getActorUri(identifier), ${wrongPropCode} name: "C" });`, + ), + rule, + ruleName, + expectedError, + }), }; } @@ -1022,6 +1313,147 @@ export function createIdRequiredEdgeCaseTests(config: TestConfig) { rule, ruleName, }), + + // ✅ If/else with id in both branches + "if else with id in both branches": lintTest({ + code: createDispatcherCode( + `\ +if (condition) { + return new Person({ id: ctx.getActorUri(identifier), name: "A" }); +} else { + return new Person({ id: ctx.getActorUri(identifier), name: "B" }); +}`, + ), + rule, + ruleName, + }), + + // ❌ If/else missing id in if block + "if else missing id in if block": lintTest({ + code: createDispatcherCode( + `\ +if (condition) { + return new Person({ name: "A" }); +} else { + return new Person({ id: ctx.getActorUri(identifier), name: "B" }); +}`, + ), + rule, + ruleName, + expectedError, + }), + + // ❌ If/else missing id in else block + "if else missing id in else block": lintTest({ + code: createDispatcherCode( + `\ +if (condition) { + return new Person({ id: ctx.getActorUri(identifier), name: "A" }); +} else { + return new Person({ name: "B" }); +}`, + ), + rule, + ruleName, + expectedError, + }), + + // ❌ If/else missing id in both blocks + "if else missing id in both blocks": lintTest({ + code: createDispatcherCode( + `\ +if (condition) { + return new Person({ name: "A" }); +} else { + return new Person({ name: "B" }); +}`, + ), + rule, + ruleName, + expectedError, + }), + + // ✅ Nested if with id + "nested if with id": lintTest({ + code: createDispatcherCode( + `\ +if (condition1) { + if (condition2) { + return new Person({ id: ctx.getActorUri(identifier), name: "A" }); + } else { + return new Person({ id: ctx.getActorUri(identifier), name: "B" }); + } +} else { + return new Person({ id: ctx.getActorUri(identifier), name: "C" }); +}`, + ), + rule, + ruleName, + }), + + // ✅ If/else if/else with id in all branches + "if else if else with id in all branches": lintTest({ + code: createDispatcherCode( + `\ +if (condition1) { + return new Person({ id: ctx.getActorUri(identifier), name: "A" }); +} else if (condition2) { + return new Person({ id: ctx.getActorUri(identifier), name: "B" }); +} else { + return new Person({ id: ctx.getActorUri(identifier), name: "C" }); +}`, + ), + rule, + ruleName, + }), + + // ❌ If/else if/else missing id in else if + "if else if else missing id in else if": lintTest({ + code: createDispatcherCode( + `\ +if (condition1) { + return new Person({ id: ctx.getActorUri(identifier), name: "A" }); +} else if (condition2) { + return new Person({ name: "B" }); +} else { + return new Person({ id: ctx.getActorUri(identifier), name: "C" }); +}`, + ), + rule, + ruleName, + expectedError, + }), + + // ✅ If/else if with final return, id in all paths + "if else if with final return id in all paths": lintTest({ + code: createDispatcherCode( + `\ +if (condition1) { + return new Person({ id: ctx.getActorUri(identifier), name: "A" }); +} else if (condition2) { + return new Person({ id: ctx.getActorUri(identifier), name: "B" }); +} +return new Person({ id: ctx.getActorUri(identifier), name: "C" });`, + ), + rule, + ruleName, + }), + + // ❌ If/else if with final return, missing id in final return + "if else if with final return missing id in final return": lintTest({ + code: createDispatcherCode( + `\ +if (condition1) { + return new Person({ id: ctx.getActorUri(identifier), name: "A" }); +} else if (condition2) { + return new Person({ id: ctx.getActorUri(identifier), name: "B" }); +} +return new Person({ name: "C" });`, + ), + rule, + ruleName, + expectedError, + }), }; } @@ -1094,6 +1526,147 @@ export function createIdMismatchEdgeCaseTests(config: TestConfig) { rule, ruleName, }), + + // ✅ If/else with correct getter in both branches + "if else with correct getter in both branches": lintTest({ + code: createDispatcherCode( + `\ +if (condition) { + return new Person({ id: ctx.getActorUri(identifier), name: "A" }); +} else { + return new Person({ id: ctx.getActorUri(identifier), name: "B" }); +}`, + ), + rule, + ruleName, + }), + + // ❌ If/else with wrong getter in if block + "if else with wrong getter in if block": lintTest({ + code: createDispatcherCode( + `\ +if (condition) { + return new Person({ id: ctx.getFollowingUri(identifier), name: "A" }); +} else { + return new Person({ id: ctx.getActorUri(identifier), name: "B" }); +}`, + ), + rule, + ruleName, + expectedError, + }), + + // ❌ If/else with wrong getter in else block + "if else with wrong getter in else block": lintTest({ + code: createDispatcherCode( + `\ +if (condition) { + return new Person({ id: ctx.getActorUri(identifier), name: "A" }); +} else { + return new Person({ id: ctx.getFollowingUri(identifier), name: "B" }); +}`, + ), + rule, + ruleName, + expectedError, + }), + + // ❌ If/else with wrong getter in both blocks + "if else with wrong getter in both blocks": lintTest({ + code: createDispatcherCode( + `\ +if (condition) { + return new Person({ id: ctx.getFollowingUri(identifier), name: "A" }); +} else { + return new Person({ id: ctx.getFollowingUri(identifier), name: "B" }); +}`, + ), + rule, + ruleName, + expectedError, + }), + + // ✅ Nested if with correct getter + "nested if with correct getter": lintTest({ + code: createDispatcherCode( + `\ +if (condition1) { + if (condition2) { + return new Person({ id: ctx.getActorUri(identifier), name: "A" }); + } else { + return new Person({ id: ctx.getActorUri(identifier), name: "B" }); + } +} else { + return new Person({ id: ctx.getActorUri(identifier), name: "C" }); +}`, + ), + rule, + ruleName, + }), + + // ✅ If/else if/else with correct getter in all branches + "if else if else with correct getter in all branches": lintTest({ + code: createDispatcherCode( + `\ +if (condition1) { + return new Person({ id: ctx.getActorUri(identifier), name: "A" }); +} else if (condition2) { + return new Person({ id: ctx.getActorUri(identifier), name: "B" }); +} else { + return new Person({ id: ctx.getActorUri(identifier), name: "C" }); +}`, + ), + rule, + ruleName, + }), + + // ❌ If/else if/else with wrong getter in else if + "if else if else with wrong getter in else if": lintTest({ + code: createDispatcherCode( + `\ +if (condition1) { + return new Person({ id: ctx.getActorUri(identifier), name: "A" }); +} else if (condition2) { + return new Person({ id: ctx.getFollowingUri(identifier), name: "B" }); +} else { + return new Person({ id: ctx.getActorUri(identifier), name: "C" }); +}`, + ), + rule, + ruleName, + expectedError, + }), + + // ✅ If/else if with final return, correct getter in all paths + "if else if with final return correct getter in all paths": lintTest({ + code: createDispatcherCode( + `\ +if (condition1) { + return new Person({ id: ctx.getActorUri(identifier), name: "A" }); +} else if (condition2) { + return new Person({ id: ctx.getActorUri(identifier), name: "B" }); +} +return new Person({ id: ctx.getActorUri(identifier), name: "C" });`, + ), + rule, + ruleName, + }), + + // ❌ If/else if with final return, wrong getter in final return + "if else if with final return wrong getter in final return": lintTest({ + code: createDispatcherCode( + `\ +if (condition1) { + return new Person({ id: ctx.getActorUri(identifier), name: "A" }); +} else if (condition2) { + return new Person({ id: ctx.getActorUri(identifier), name: "B" }); +} +return new Person({ id: ctx.getFollowingUri(identifier), name: "C" });`, + ), + rule, + ruleName, + expectedError, + }), }; } @@ -1178,5 +1751,155 @@ return condition1 rule, ruleName, }), + + // ✅ If/else with property in both branches + "if else with property in both branches": lintTest({ + code: createChainedDispatcherCode( + `\ +if (condition) { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); +} else { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); +}`, + "setKeyPairsDispatcher", + ), + rule, + ruleName, + }), + + // ❌ If/else missing property in if block + "if else missing property in if block": lintTest({ + code: createChainedDispatcherCode( + `\ +if (condition) { + return new Person({ id: ctx.getActorUri(identifier), name: "A" }); +} else { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); +}`, + "setKeyPairsDispatcher", + ), + rule, + ruleName, + expectedError, + }), + + // ❌ If/else missing property in else block + "if else missing property in else block": lintTest({ + code: createChainedDispatcherCode( + `\ +if (condition) { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); +} else { + return new Person({ id: ctx.getActorUri(identifier), name: "B" }); +}`, + "setKeyPairsDispatcher", + ), + rule, + ruleName, + expectedError, + }), + + // ❌ If/else missing property in both blocks + "if else missing property in both blocks": lintTest({ + code: createChainedDispatcherCode( + `\ +if (condition) { + return new Person({ id: ctx.getActorUri(identifier), name: "A" }); +} else { + return new Person({ id: ctx.getActorUri(identifier), name: "B" }); +}`, + "setKeyPairsDispatcher", + ), + rule, + ruleName, + expectedError, + }), + + // ✅ Nested if with property + "nested if with property": lintTest({ + code: createChainedDispatcherCode( + `\ +if (condition1) { + if (condition2) { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); + } else { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); + } +} else { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" }); +}`, + "setKeyPairsDispatcher", + ), + rule, + ruleName, + }), + + // ✅ If/else if/else with property in all branches + "if else if else with property in all branches": lintTest({ + code: createChainedDispatcherCode( + `\ +if (condition1) { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); +} else if (condition2) { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); +} else { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" }); +}`, + "setKeyPairsDispatcher", + ), + rule, + ruleName, + }), + + // ❌ If/else if/else missing property in else if + "if else if else missing property in else if": lintTest({ + code: createChainedDispatcherCode( + `\ +if (condition1) { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); +} else if (condition2) { + return new Person({ id: ctx.getActorUri(identifier), name: "B" }); +} else { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" }); +}`, + "setKeyPairsDispatcher", + ), + rule, + ruleName, + expectedError, + }), + + // ✅ If/else if with final return, property in all paths + "if else if with final return property in all paths": lintTest({ + code: createChainedDispatcherCode( + `\ +if (condition1) { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); +} else if (condition2) { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); +} +return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" });`, + "setKeyPairsDispatcher", + ), + rule, + ruleName, + }), + + // ❌ If/else if with final return, missing property in final return + "if else if with final return missing property in final return": lintTest({ + code: createChainedDispatcherCode( + `\ +if (condition1) { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); +} else if (condition2) { + return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); +} +return new Person({ id: ctx.getActorUri(identifier), name: "C" });`, + "setKeyPairsDispatcher", + ), + rule, + ruleName, + expectedError, + }), }; } diff --git a/packages/lint/src/tests/actor-assertion-method-required.test.ts b/packages/lint/src/tests/actor-assertion-method-required.test.ts index 474e0289a..ba8b01855 100644 --- a/packages/lint/src/tests/actor-assertion-method-required.test.ts +++ b/packages/lint/src/tests/actor-assertion-method-required.test.ts @@ -1,6 +1,7 @@ import { test } from "node:test"; import { properties, RULE_IDS } from "../lib/const.ts"; import { actorPropertyRequired } from "../lib/messages.ts"; +import { createKeyRequiredEdgeCaseTests } from "../lib/test-templates.ts"; import lintTest from "../lib/test.ts"; import * as rule from "../rules/actor-assertion-method-required.ts"; @@ -201,3 +202,63 @@ test( expectedError: actorPropertyRequired(properties.assertionMethod), }), ); + +// Edge case tests +const config = { rule, ruleName }; +const edgeCases = createKeyRequiredEdgeCaseTests("assertionMethod", config); +test( + `${ruleName}: ✅ Edge - ternary with property in both branches`, + edgeCases["ternary with property in both branches"], +); +test( + `${ruleName}: ❌ Edge - ternary missing property in consequent`, + edgeCases["ternary missing property in consequent"], +); +test( + `${ruleName}: ❌ Edge - ternary missing property in alternate`, + edgeCases["ternary missing property in alternate"], +); +test( + `${ruleName}: ❌ Edge - ternary missing property in both branches`, + edgeCases["ternary missing property in both branches"], +); +test( + `${ruleName}: ✅ Edge - nested ternary with property`, + edgeCases["nested ternary with property"], +); +test( + `${ruleName}: ✅ Edge - if/else with property in both branches`, + edgeCases["if else with property in both branches"], +); +test( + `${ruleName}: ❌ Edge - if/else missing property in if block`, + edgeCases["if else missing property in if block"], +); +test( + `${ruleName}: ❌ Edge - if/else missing property in else block`, + edgeCases["if else missing property in else block"], +); +test( + `${ruleName}: ❌ Edge - if/else missing property in both blocks`, + edgeCases["if else missing property in both blocks"], +); +test( + `${ruleName}: ✅ Edge - nested if with property`, + edgeCases["nested if with property"], +); +test( + `${ruleName}: ✅ Edge - if else if else with property in all branches`, + edgeCases["if else if else with property in all branches"], +); +test( + `${ruleName}: ❌ Edge - if else if else missing property in else if`, + edgeCases["if else if else missing property in else if"], +); +test( + `${ruleName}: ✅ Edge - if else if with final return property in all paths`, + edgeCases["if else if with final return property in all paths"], +); +test( + `${ruleName}: ❌ Edge - if else if with final return missing property in final return`, + edgeCases["if else if with final return missing property in final return"], +); diff --git a/packages/lint/src/tests/actor-featured-property-mismatch.test.ts b/packages/lint/src/tests/actor-featured-property-mismatch.test.ts index ae04a936e..02c659a00 100644 --- a/packages/lint/src/tests/actor-featured-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-featured-property-mismatch.test.ts @@ -49,3 +49,39 @@ test( `${ruleName}: ✅ Edge - nested ternary with correct getter`, edgeCases["nested ternary with correct getter"], ); +test( + `${ruleName}: ✅ Edge - if/else with correct getter in both branches`, + edgeCases["if else with correct getter in both branches"], +); +test( + `${ruleName}: ❌ Edge - if/else with wrong getter in if block`, + edgeCases["if else with wrong getter in if block"], +); +test( + `${ruleName}: ❌ Edge - if/else with wrong getter in else block`, + edgeCases["if else with wrong getter in else block"], +); +test( + `${ruleName}: ❌ Edge - if/else with wrong getter in both blocks`, + edgeCases["if else with wrong getter in both blocks"], +); +test( + `${ruleName}: ✅ Edge - nested if with correct getter`, + edgeCases["nested if with correct getter"], +); +test( + `${ruleName}: ✅ Edge - if else if else with correct getter in all branches`, + edgeCases["if else if else with correct getter in all branches"], +); +test( + `${ruleName}: ❌ Edge - if else if else with wrong getter in else if`, + edgeCases["if else if else with wrong getter in else if"], +); +test( + `${ruleName}: ✅ Edge - if else if with final return correct getter in all paths`, + edgeCases["if else if with final return correct getter in all paths"], +); +test( + `${ruleName}: ❌ Edge - if else if with final return wrong getter in final return`, + edgeCases["if else if with final return wrong getter in final return"], +); diff --git a/packages/lint/src/tests/actor-featured-property-required.test.ts b/packages/lint/src/tests/actor-featured-property-required.test.ts index e1a522010..22e79310a 100644 --- a/packages/lint/src/tests/actor-featured-property-required.test.ts +++ b/packages/lint/src/tests/actor-featured-property-required.test.ts @@ -79,3 +79,39 @@ test( `${ruleName}: ✅ Edge - nested ternary with property`, edgeCases["nested ternary with property"], ); +test( + `${ruleName}: ✅ Edge - if/else with property in both branches`, + edgeCases["if else with property in both branches"], +); +test( + `${ruleName}: ❌ Edge - if/else missing property in if block`, + edgeCases["if else missing property in if block"], +); +test( + `${ruleName}: ❌ Edge - if/else missing property in else block`, + edgeCases["if else missing property in else block"], +); +test( + `${ruleName}: ❌ Edge - if/else missing property in both blocks`, + edgeCases["if else missing property in both blocks"], +); +test( + `${ruleName}: ✅ Edge - nested if with property`, + edgeCases["nested if with property"], +); +test( + `${ruleName}: ✅ Edge - if else if else with featured in all branches`, + edgeCases["if else if else with property in all branches"], +); +test( + `${ruleName}: ❌ Edge - if else if else missing featured in else if`, + edgeCases["if else if else missing property in else if"], +); +test( + `${ruleName}: ✅ Edge - if else if with final return featured in all paths`, + edgeCases["if else if with final return property in all paths"], +); +test( + `${ruleName}: ❌ Edge - if else if with final return missing featured in final return`, + edgeCases["if else if with final return missing property in final return"], +); diff --git a/packages/lint/src/tests/actor-featured-tags-property-mismatch.test.ts b/packages/lint/src/tests/actor-featured-tags-property-mismatch.test.ts index a60fc2f6c..1322eead4 100644 --- a/packages/lint/src/tests/actor-featured-tags-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-featured-tags-property-mismatch.test.ts @@ -49,3 +49,39 @@ test( `${ruleName}: ✅ Edge - nested ternary with correct getter`, edgeCases["nested ternary with correct getter"], ); +test( + `${ruleName}: ✅ Edge - if/else with correct getter in both branches`, + edgeCases["if else with correct getter in both branches"], +); +test( + `${ruleName}: ❌ Edge - if/else with wrong getter in if block`, + edgeCases["if else with wrong getter in if block"], +); +test( + `${ruleName}: ❌ Edge - if/else with wrong getter in else block`, + edgeCases["if else with wrong getter in else block"], +); +test( + `${ruleName}: ❌ Edge - if/else with wrong getter in both blocks`, + edgeCases["if else with wrong getter in both blocks"], +); +test( + `${ruleName}: ✅ Edge - nested if with correct getter`, + edgeCases["nested if with correct getter"], +); +test( + `${ruleName}: ✅ Edge - if else if else with correct getter in all branches`, + edgeCases["if else if else with correct getter in all branches"], +); +test( + `${ruleName}: ❌ Edge - if else if else with wrong getter in else if`, + edgeCases["if else if else with wrong getter in else if"], +); +test( + `${ruleName}: ✅ Edge - if else if with final return correct getter in all paths`, + edgeCases["if else if with final return correct getter in all paths"], +); +test( + `${ruleName}: ❌ Edge - if else if with final return wrong getter in final return`, + edgeCases["if else if with final return wrong getter in final return"], +); diff --git a/packages/lint/src/tests/actor-featured-tags-property-required.test.ts b/packages/lint/src/tests/actor-featured-tags-property-required.test.ts index 7f524ae26..1caafc59d 100644 --- a/packages/lint/src/tests/actor-featured-tags-property-required.test.ts +++ b/packages/lint/src/tests/actor-featured-tags-property-required.test.ts @@ -79,3 +79,39 @@ test( `${ruleName}: ✅ Edge - nested ternary with property`, edgeCases["nested ternary with property"], ); +test( + `${ruleName}: ✅ Edge - if/else with property in both branches`, + edgeCases["if else with property in both branches"], +); +test( + `${ruleName}: ❌ Edge - if/else missing property in if block`, + edgeCases["if else missing property in if block"], +); +test( + `${ruleName}: ❌ Edge - if/else missing property in else block`, + edgeCases["if else missing property in else block"], +); +test( + `${ruleName}: ❌ Edge - if/else missing property in both blocks`, + edgeCases["if else missing property in both blocks"], +); +test( + `${ruleName}: ✅ Edge - nested if with property`, + edgeCases["nested if with property"], +); +test( + `${ruleName}: ✅ Edge - if else if else with featuredTags in all branches`, + edgeCases["if else if else with property in all branches"], +); +test( + `${ruleName}: ❌ Edge - if else if else missing featuredTags in else if`, + edgeCases["if else if else missing property in else if"], +); +test( + `${ruleName}: ✅ Edge - if else if with final return featuredTags in all paths`, + edgeCases["if else if with final return property in all paths"], +); +test( + `${ruleName}: ❌ Edge - if else if with final return missing featuredTags in final return`, + edgeCases["if else if with final return missing property in final return"], +); diff --git a/packages/lint/src/tests/actor-followers-property-mismatch.test.ts b/packages/lint/src/tests/actor-followers-property-mismatch.test.ts index 04fe04cf7..d7c4c0829 100644 --- a/packages/lint/src/tests/actor-followers-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-followers-property-mismatch.test.ts @@ -49,3 +49,39 @@ test( `${ruleName}: ✅ Edge - nested ternary with correct getter`, edgeCases["nested ternary with correct getter"], ); +test( + `${ruleName}: ✅ Edge - if/else with correct getter in both branches`, + edgeCases["if else with correct getter in both branches"], +); +test( + `${ruleName}: ❌ Edge - if/else with wrong getter in if block`, + edgeCases["if else with wrong getter in if block"], +); +test( + `${ruleName}: ❌ Edge - if/else with wrong getter in else block`, + edgeCases["if else with wrong getter in else block"], +); +test( + `${ruleName}: ❌ Edge - if/else with wrong getter in both blocks`, + edgeCases["if else with wrong getter in both blocks"], +); +test( + `${ruleName}: ✅ Edge - nested if with correct getter`, + edgeCases["nested if with correct getter"], +); +test( + `${ruleName}: ✅ Edge - if else if else with correct getter in all branches`, + edgeCases["if else if else with correct getter in all branches"], +); +test( + `${ruleName}: ❌ Edge - if else if else with wrong getter in else if`, + edgeCases["if else if else with wrong getter in else if"], +); +test( + `${ruleName}: ✅ Edge - if else if with final return correct getter in all paths`, + edgeCases["if else if with final return correct getter in all paths"], +); +test( + `${ruleName}: ❌ Edge - if else if with final return wrong getter in final return`, + edgeCases["if else if with final return wrong getter in final return"], +); diff --git a/packages/lint/src/tests/actor-followers-property-required.test.ts b/packages/lint/src/tests/actor-followers-property-required.test.ts index 87cf0030f..5370497f2 100644 --- a/packages/lint/src/tests/actor-followers-property-required.test.ts +++ b/packages/lint/src/tests/actor-followers-property-required.test.ts @@ -79,3 +79,39 @@ test( `${ruleName}: ✅ Edge - nested ternary with property`, edgeCases["nested ternary with property"], ); +test( + `${ruleName}: ✅ Edge - if/else with property in both branches`, + edgeCases["if else with property in both branches"], +); +test( + `${ruleName}: ❌ Edge - if/else missing property in if block`, + edgeCases["if else missing property in if block"], +); +test( + `${ruleName}: ❌ Edge - if/else missing property in else block`, + edgeCases["if else missing property in else block"], +); +test( + `${ruleName}: ❌ Edge - if/else missing property in both blocks`, + edgeCases["if else missing property in both blocks"], +); +test( + `${ruleName}: ✅ Edge - nested if with property`, + edgeCases["nested if with property"], +); +test( + `${ruleName}: ✅ Edge - if else if else with followers in all branches`, + edgeCases["if else if else with property in all branches"], +); +test( + `${ruleName}: ❌ Edge - if else if else missing followers in else if`, + edgeCases["if else if else missing property in else if"], +); +test( + `${ruleName}: ✅ Edge - if else if with final return followers in all paths`, + edgeCases["if else if with final return property in all paths"], +); +test( + `${ruleName}: ❌ Edge - if else if with final return missing followers in final return`, + edgeCases["if else if with final return missing property in final return"], +); diff --git a/packages/lint/src/tests/actor-following-property-mismatch.test.ts b/packages/lint/src/tests/actor-following-property-mismatch.test.ts index a7d5e4cfc..e7fe65d37 100644 --- a/packages/lint/src/tests/actor-following-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-following-property-mismatch.test.ts @@ -49,3 +49,39 @@ test( `${ruleName}: ✅ Edge - nested ternary with correct getter`, edgeCases["nested ternary with correct getter"], ); +test( + `${ruleName}: ✅ Edge - if/else with correct getter in both branches`, + edgeCases["if else with correct getter in both branches"], +); +test( + `${ruleName}: ❌ Edge - if/else with wrong getter in if block`, + edgeCases["if else with wrong getter in if block"], +); +test( + `${ruleName}: ❌ Edge - if/else with wrong getter in else block`, + edgeCases["if else with wrong getter in else block"], +); +test( + `${ruleName}: ❌ Edge - if/else with wrong getter in both blocks`, + edgeCases["if else with wrong getter in both blocks"], +); +test( + `${ruleName}: ✅ Edge - nested if with correct getter`, + edgeCases["nested if with correct getter"], +); +test( + `${ruleName}: ✅ Edge - if else if else with correct getter in all branches`, + edgeCases["if else if else with correct getter in all branches"], +); +test( + `${ruleName}: ❌ Edge - if else if else with wrong getter in else if`, + edgeCases["if else if else with wrong getter in else if"], +); +test( + `${ruleName}: ✅ Edge - if else if with final return correct getter in all paths`, + edgeCases["if else if with final return correct getter in all paths"], +); +test( + `${ruleName}: ❌ Edge - if else if with final return wrong getter in final return`, + edgeCases["if else if with final return wrong getter in final return"], +); diff --git a/packages/lint/src/tests/actor-following-property-required.test.ts b/packages/lint/src/tests/actor-following-property-required.test.ts index a49483578..03855ed4a 100644 --- a/packages/lint/src/tests/actor-following-property-required.test.ts +++ b/packages/lint/src/tests/actor-following-property-required.test.ts @@ -79,3 +79,39 @@ test( `${ruleName}: ✅ Edge - nested ternary with property`, edgeCases["nested ternary with property"], ); +test( + `${ruleName}: ✅ Edge - if/else with property in both branches`, + edgeCases["if else with property in both branches"], +); +test( + `${ruleName}: ❌ Edge - if/else missing property in if block`, + edgeCases["if else missing property in if block"], +); +test( + `${ruleName}: ❌ Edge - if/else missing property in else block`, + edgeCases["if else missing property in else block"], +); +test( + `${ruleName}: ❌ Edge - if/else missing property in both blocks`, + edgeCases["if else missing property in both blocks"], +); +test( + `${ruleName}: ✅ Edge - nested if with property`, + edgeCases["nested if with property"], +); +test( + `${ruleName}: ✅ Edge - if else if else with following in all branches`, + edgeCases["if else if else with property in all branches"], +); +test( + `${ruleName}: ❌ Edge - if else if else missing following in else if`, + edgeCases["if else if else missing property in else if"], +); +test( + `${ruleName}: ✅ Edge - if else if with final return following in all paths`, + edgeCases["if else if with final return property in all paths"], +); +test( + `${ruleName}: ❌ Edge - if else if with final return missing following in final return`, + edgeCases["if else if with final return missing property in final return"], +); diff --git a/packages/lint/src/tests/actor-id-mismatch.test.ts b/packages/lint/src/tests/actor-id-mismatch.test.ts index 4b7a4ee93..e11960a8a 100644 --- a/packages/lint/src/tests/actor-id-mismatch.test.ts +++ b/packages/lint/src/tests/actor-id-mismatch.test.ts @@ -1,216 +1,96 @@ import { test } from "node:test"; -import { properties, RULE_IDS } from "../lib/const.ts"; -import { actorPropertyMismatch } from "../lib/messages.ts"; -import lintTest from "../lib/test.ts"; +import { RULE_IDS } from "../lib/const.ts"; +import { + createIdMismatchEdgeCaseTests, + createIdMismatchRuleTests, +} from "../lib/test-templates.ts"; import * as rule from "../rules/actor-id-mismatch.ts"; const ruleName = RULE_IDS.actorIdMismatch; +const config = { rule, ruleName }; -const expectedError = actorPropertyMismatch({ - path: properties.id.path.join("."), - ctxName: "ctx", - idName: "identifier", - methodName: properties.id.getter, - requiresIdentifier: properties.id.requiresIdentifier, -}); - +// Standard id mismatch rule tests +const tests = createIdMismatchRuleTests(config); test( - `${ruleName}: ✅ Good - \`setActorDispatcher\` called on non-Federation object`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ - id: "https://example.com/users/123", - name: "John Doe", - }); - }); - `, - rule, - ruleName, - federationSetup: ` - const federation = { - setActorDispatcher: () => {} - }; - `, - }), + `${ruleName}: ✅ Good - non-federation object`, + tests["non-federation object"], ); - test( - `${ruleName}: ✅ Good - id uses ctx.getActorUri(identifier)`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ - id: ctx.getActorUri(identifier), - name: "John Doe", - }); - }); - `, - rule, - ruleName, - }), + `${ruleName}: ✅ Good - correct getter used`, + tests["correct getter used"], ); - test( - `${ruleName}: ✅ Good - BlockStatement with correct id`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - const name = "John Doe"; - return new Person({ - id: ctx.getActorUri(identifier), - name, - }); - }); - `, - rule, - ruleName, - }), + `${ruleName}: ❌ Bad - literal string id`, + tests["literal string id"], ); - test( - `${ruleName}: ❌ Bad - id uses hardcoded string instead of ctx.getActorUri()`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ - id: "https://example.com/users/123", - name: "John Doe", - }); - }); - `, - rule, - ruleName, - expectedError: expectedError, - }), + `${ruleName}: ❌ Bad - new URL as id`, + tests["new URL as id"], ); - test( - `${ruleName}: ❌ Bad - id uses wrong method (getInboxUri instead of getActorUri)`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ - id: ctx.getInboxUri(identifier), - name: "John Doe", - }); - }); - `, - rule, - ruleName, - expectedError: expectedError, - }), + `${ruleName}: ❌ Bad - wrong getter used`, + tests["wrong getter used"], ); - test( - `${ruleName}: ❌ Bad - id uses wrong identifier parameter`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ - id: ctx.getActorUri("wrong"), - name: "John Doe", - }); - }); - `, - rule, - ruleName, - expectedError: expectedError, - }), + `${ruleName}: ❌ Bad - wrong identifier`, + tests["wrong identifier"], ); +// Edge case tests +const edgeCases = createIdMismatchEdgeCaseTests(config); test( - `${ruleName} Edge: ✅ multiple return statements - all correct`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - if (identifier === "admin") { - return new Person({ id: ctx.getActorUri(identifier), name: "Admin" }); - } - return new Person({ id: ctx.getActorUri(identifier), name: "User" }); - }); - `, - rule, - ruleName, - }), + `${ruleName}: ✅ Edge - ternary with correct getter in both branches`, + edgeCases["ternary with correct getter in both branches"], ); - test( - `${ruleName} Edge: ⚠️ multiple returns - known limitation`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - if (identifier === "admin") { - return new Person({ id: "hardcoded", name: "Admin" }); - } - return new Person({ id: ctx.getActorUri(identifier), name: "User" }); - }); - `, - rule, - ruleName, - // Known limitation: Once ANY return has correct id, the rule passes. - // The first return with wrong id is not caught. - }), + `${ruleName}: ❌ Edge - ternary with wrong getter in consequent`, + edgeCases["ternary with wrong getter in consequent"], ); - test( - `${ruleName} Edge: ✅ spread operator with correct id after spread`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - const base = { name: "User" }; - return new Person({ ...base, id: ctx.getActorUri(identifier) }); - }); - `, - rule, - ruleName, - }), + `${ruleName}: ❌ Edge - ternary with wrong getter in alternate`, + edgeCases["ternary with wrong getter in alternate"], ); - test( - `${ruleName} Edge: ❌ spread operator with wrong id after spread`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - const base = { name: "User" }; - return new Person({ ...base, id: "hardcoded" }); - }); - `, - rule, - ruleName, - expectedError: expectedError, - }), + `${ruleName}: ❌ Edge - ternary with wrong getter in both branches`, + edgeCases["ternary with wrong getter in both branches"], ); - test( - `${ruleName} Edge: ✅ arrow function direct return with correct id`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ - id: ctx.getActorUri(identifier), - name: "User", - }); - }); - `, - rule, - ruleName, - }), + `${ruleName}: ✅ Edge - nested ternary with correct getter`, + edgeCases["nested ternary with correct getter"], +); +test( + `${ruleName}: ✅ Edge - if/else with correct getter in both branches`, + edgeCases["if else with correct getter in both branches"], +); +test( + `${ruleName}: ❌ Edge - if/else with wrong getter in if block`, + edgeCases["if else with wrong getter in if block"], +); +test( + `${ruleName}: ❌ Edge - if/else with wrong getter in else block`, + edgeCases["if else with wrong getter in else block"], +); +test( + `${ruleName}: ❌ Edge - if/else with wrong getter in both blocks`, + edgeCases["if else with wrong getter in both blocks"], +); +test( + `${ruleName}: ✅ Edge - nested if with correct getter`, + edgeCases["nested if with correct getter"], +); +test( + `${ruleName}: ✅ Edge - if else if else with correct getter in all branches`, + edgeCases["if else if else with correct getter in all branches"], +); +test( + `${ruleName}: ❌ Edge - if else if else with wrong getter in else if`, + edgeCases["if else if else with wrong getter in else if"], +); +test( + `${ruleName}: ✅ Edge - if else if with final return correct getter in all paths`, + edgeCases["if else if with final return correct getter in all paths"], ); - test( - `${ruleName} Edge: ❌ arrow function direct return with wrong id`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ - id: "hardcoded", - name: "User", - }); - }); - `, - rule, - ruleName, - expectedError: expectedError, - }), + `${ruleName}: ❌ Edge - if else if with final return wrong getter in final return`, + edgeCases["if else if with final return wrong getter in final return"], ); diff --git a/packages/lint/src/tests/actor-id-required.test.ts b/packages/lint/src/tests/actor-id-required.test.ts index ac7d94dd3..52fe38572 100644 --- a/packages/lint/src/tests/actor-id-required.test.ts +++ b/packages/lint/src/tests/actor-id-required.test.ts @@ -1,359 +1,101 @@ import { test } from "node:test"; -import { properties, RULE_IDS } from "../lib/const.ts"; -import { actorPropertyRequired } from "../lib/messages.ts"; -import lintTest from "../lib/test.ts"; +import { RULE_IDS } from "../lib/const.ts"; +import { + createIdRequiredEdgeCaseTests, + createIdRequiredRuleTests, +} from "../lib/test-templates.ts"; import * as rule from "../rules/actor-id-required.ts"; const ruleName = RULE_IDS.actorIdRequired; +const config = { rule, ruleName }; +// Standard id required rule tests +const tests = createIdRequiredRuleTests(config); test( - `${ruleName}: ✅ Good - \`setActorDispatcher\` called on non-Federation object`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ - name: "John Doe", - }); - }); - `, - rule, - ruleName, - federationSetup: ` - const federation = { - setActorDispatcher: () => {} - }; - `, - }), + `${ruleName}: ✅ Good - non-federation object`, + tests["non-federation object"], ); - -test( - `${ruleName}: ✅ Good - with \`id\` property (any value)`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ - id: "https://example.com/users/123", - name: "John Doe", - }); - }); - `, - rule, - ruleName, - }), -); - test( - `${ruleName}: ✅ Good - with \`id\` property using ctx.getActorUri()`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ - id: ctx.getActorUri(identifier), - name: "John Doe", - }); - }); - `, - rule, - ruleName, - }), + `${ruleName}: ✅ Good - with id property any value`, + tests["with id property any value"], ); - test( - `${ruleName}: ✅ Good - BlockStatement with \`id\``, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - const name = "John Doe"; - return new Person({ - id: ctx.getActorUri(identifier), - name, - }); - }); - `, - rule, - ruleName, - }), + `${ruleName}: ✅ Good - with id property using getActorUri`, + tests["with id property using getActorUri"], ); - test( - `${ruleName}: ❌ Bad - without \`id\` property`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ - name: "John Doe", - }); - }); - `, - rule, - ruleName, - expectedError: actorPropertyRequired(properties.id), - }), + `${ruleName}: ✅ Good - block statement with id`, + tests["block statement with id"], ); - +test(`${ruleName}: ❌ Bad - without id property`, tests["without id property"]); test( `${ruleName}: ❌ Bad - returning empty object`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({}); - }); - `, - rule, - ruleName, - expectedError: actorPropertyRequired(properties.id), - }), + tests["returning empty object"], ); - test( - `${ruleName}: ✅ Good - multiple properties including \`id\``, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ - id: ctx.getActorUri(identifier), - name: "John Doe", - inbox: ctx.getInboxUri(identifier), - outbox: ctx.getOutboxUri(identifier), - }); - }); - `, - rule, - ruleName, - }), + `${ruleName}: ✅ Good - multiple properties including id`, + tests["multiple properties including id"], ); - test( - `${ruleName}: ❌ Bad - variable assignment without \`id\``, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - const actor = new Person({ - name: "John Doe", - }); - return actor; - }); - `, - rule, - ruleName, - expectedError: actorPropertyRequired(properties.id), - }), + `${ruleName}: ❌ Bad - variable assignment without id`, + tests["variable assignment without id"], ); -const withId = 'new Person({ id: ctx.getActorUri(identifier), name: "User" })'; -const withoutId = 'new Person({ name: "User" })'; +// Edge case tests +const edgeCases = createIdRequiredEdgeCaseTests(config); test( - `${ruleName}: ✅ Edge - multiple return statements - all have id`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - if (identifier === "admin") { - return ${withId}; - } - return identifier === "admin" - ? ${withId} - : ${withId}; - }); - `, - rule, - ruleName, - }), + `${ruleName}: ✅ Edge - ternary with id in both branches`, + edgeCases["ternary with id in both branches"], ); - test( - `${ruleName}: ✅ Edge - multiple return statements - first missing id (known limitation)`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - if (identifier === "admin") { - return ${withoutId}; - } - return ${withId}; - }); - `, - rule, - ruleName, - }), + `${ruleName}: ❌ Edge - ternary missing id in consequent`, + edgeCases["ternary missing id in consequent"], ); - test( - `${ruleName}: ❌ Edge - multiple return statements - second missing id`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - if (identifier === "admin") { - return ${withId}; - } - return ${withoutId}; - }); - `, - rule, - ruleName, - expectedError: actorPropertyRequired(properties.id), - }), + `${ruleName}: ❌ Edge - ternary missing id in alternate`, + edgeCases["ternary missing id in alternate"], ); - test( - `${ruleName}: ✅ Edge - if/else with else block (known limitation: else not checked)`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - if (identifier) { - return ${withId}; - } else { - return ${withoutId}; - } - return ${withId}; - }); - `, - rule, - ruleName, - }), + `${ruleName}: ❌ Edge - ternary missing id in both branches`, + edgeCases["ternary missing id in both branches"], ); - test( - `${ruleName}: ✅ Edge - nested if with id`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - if (identifier) { - if (identifier === "admin") { - return ${withId}; - } - return ${withId}; - } - return ${withId}; - }); - `, - rule, - ruleName, - }), + `${ruleName}: ✅ Edge - nested ternary with id`, + edgeCases["nested ternary with id"], ); - test( - `${ruleName}: ✅ Edge - ternary operator with id in both branches`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return identifier ? ${withId} : ${withId}; - }); - `, - rule, - ruleName, - }), + `${ruleName}: ✅ Edge - if/else with id in both branches`, + edgeCases["if else with id in both branches"], ); - test( - `${ruleName}: ❌ Edge - ternary operator without id in consequent`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return identifier ? ${withoutId} : ${withId}; - }); - `, - rule, - ruleName, - expectedError: actorPropertyRequired(properties.id), - }), + `${ruleName}: ❌ Edge - if/else missing id in if block`, + edgeCases["if else missing id in if block"], ); - test( - `${ruleName}: ❌ Edge - ternary operator without id in alternate`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return identifier ? ${withId} : ${withoutId}; - }); - `, - rule, - ruleName, - expectedError: actorPropertyRequired(properties.id), - }), + `${ruleName}: ❌ Edge - if/else missing id in else block`, + edgeCases["if else missing id in else block"], ); - test( - `${ruleName}: ✅ Edge - spread operator with id property after spread`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - const base = { name: "User" }; - return new Person({ ...base, id: ctx.getActorUri(identifier) }); - }); - `, - rule, - ruleName, - }), + `${ruleName}: ❌ Edge - if/else missing id in both blocks`, + edgeCases["if else missing id in both blocks"], ); - test( - `${ruleName}: ❌ Edge - spread operator with id in spread source (known limitation)`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - const base = { id: ctx.getActorUri(identifier), name: "User" }; - return new Person({ ...base }); - }); - `, - rule, - ruleName, - expectedError: actorPropertyRequired(properties.id), - }), + `${ruleName}: ✅ Edge - nested if with id`, + edgeCases["nested if with id"], ); - test( - `${ruleName}: ❌ Edge - variable assignment then return (known limitation)`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - const actor = ${withId}; - return actor; - }); - `, - rule, - ruleName, - expectedError: actorPropertyRequired(properties.id), - }), + `${ruleName}: ✅ Edge - if else if else with id in all branches`, + edgeCases["if else if else with id in all branches"], ); - test( - `${ruleName}: ❌ Edge - property assignment after construction (known limitation)`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - const actor = ${withoutId}; - actor.id = ctx.getActorUri(identifier); - return actor; - }); - `, - rule, - ruleName, - expectedError: actorPropertyRequired(properties.id), - }), + `${ruleName}: ❌ Edge - if else if else missing id in else if`, + edgeCases["if else if else missing id in else if"], ); - test( - `${ruleName}: ✅ Edge - arrow function direct return NewExpression`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => - ${withId} - ); - `, - rule, - ruleName, - }), + `${ruleName}: ✅ Edge - if else if with final return id in all paths`, + edgeCases["if else if with final return id in all paths"], ); - test( - `${ruleName}: ✅ Edge - return null (no actor)`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - if (!identifier) return null; - return ${withId}; - }); - `, - rule, - ruleName, - }), + `${ruleName}: ❌ Edge - if else if with final return missing id in final return`, + edgeCases["if else if with final return missing id in final return"], ); diff --git a/packages/lint/src/tests/actor-inbox-property-mismatch.test.ts b/packages/lint/src/tests/actor-inbox-property-mismatch.test.ts index 977df1981..5b1918926 100644 --- a/packages/lint/src/tests/actor-inbox-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-inbox-property-mismatch.test.ts @@ -49,3 +49,39 @@ test( `${ruleName}: ✅ Edge - nested ternary with correct getter`, edgeCases["nested ternary with correct getter"], ); +test( + `${ruleName}: ✅ Edge - if/else with correct getter in both branches`, + edgeCases["if else with correct getter in both branches"], +); +test( + `${ruleName}: ❌ Edge - if/else with wrong getter in if block`, + edgeCases["if else with wrong getter in if block"], +); +test( + `${ruleName}: ❌ Edge - if/else with wrong getter in else block`, + edgeCases["if else with wrong getter in else block"], +); +test( + `${ruleName}: ❌ Edge - if/else with wrong getter in both blocks`, + edgeCases["if else with wrong getter in both blocks"], +); +test( + `${ruleName}: ✅ Edge - nested if with correct getter`, + edgeCases["nested if with correct getter"], +); +test( + `${ruleName}: ✅ Edge - if else if else with correct getter in all branches`, + edgeCases["if else if else with correct getter in all branches"], +); +test( + `${ruleName}: ❌ Edge - if else if else with wrong getter in else if`, + edgeCases["if else if else with wrong getter in else if"], +); +test( + `${ruleName}: ✅ Edge - if else if with final return correct getter in all paths`, + edgeCases["if else if with final return correct getter in all paths"], +); +test( + `${ruleName}: ❌ Edge - if else if with final return wrong getter in final return`, + edgeCases["if else if with final return wrong getter in final return"], +); diff --git a/packages/lint/src/tests/actor-inbox-property-required.test.ts b/packages/lint/src/tests/actor-inbox-property-required.test.ts index 5e8f2683f..543b3215f 100644 --- a/packages/lint/src/tests/actor-inbox-property-required.test.ts +++ b/packages/lint/src/tests/actor-inbox-property-required.test.ts @@ -79,3 +79,39 @@ test( `${ruleName}: ✅ Edge - nested ternary with property`, edgeCases["nested ternary with property"], ); +test( + `${ruleName}: ✅ Edge - if/else with property in both branches`, + edgeCases["if else with property in both branches"], +); +test( + `${ruleName}: ❌ Edge - if/else missing property in if block`, + edgeCases["if else missing property in if block"], +); +test( + `${ruleName}: ❌ Edge - if/else missing property in else block`, + edgeCases["if else missing property in else block"], +); +test( + `${ruleName}: ❌ Edge - if/else missing property in both blocks`, + edgeCases["if else missing property in both blocks"], +); +test( + `${ruleName}: ✅ Edge - nested if with property`, + edgeCases["nested if with property"], +); +test( + `${ruleName}: ✅ Edge - if else if else with inbox in all branches`, + edgeCases["if else if else with property in all branches"], +); +test( + `${ruleName}: ❌ Edge - if else if else missing inbox in else if`, + edgeCases["if else if else missing property in else if"], +); +test( + `${ruleName}: ✅ Edge - if else if with final return inbox in all paths`, + edgeCases["if else if with final return property in all paths"], +); +test( + `${ruleName}: ❌ Edge - if else if with final return missing inbox in final return`, + edgeCases["if else if with final return missing property in final return"], +); diff --git a/packages/lint/src/tests/actor-liked-property-mismatch.test.ts b/packages/lint/src/tests/actor-liked-property-mismatch.test.ts index 6d6a872b2..443fdffc0 100644 --- a/packages/lint/src/tests/actor-liked-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-liked-property-mismatch.test.ts @@ -49,3 +49,39 @@ test( `${ruleName}: ✅ Edge - nested ternary with correct getter`, edgeCases["nested ternary with correct getter"], ); +test( + `${ruleName}: ✅ Edge - if/else with correct getter in both branches`, + edgeCases["if else with correct getter in both branches"], +); +test( + `${ruleName}: ❌ Edge - if/else with wrong getter in if block`, + edgeCases["if else with wrong getter in if block"], +); +test( + `${ruleName}: ❌ Edge - if/else with wrong getter in else block`, + edgeCases["if else with wrong getter in else block"], +); +test( + `${ruleName}: ❌ Edge - if/else with wrong getter in both blocks`, + edgeCases["if else with wrong getter in both blocks"], +); +test( + `${ruleName}: ✅ Edge - nested if with correct getter`, + edgeCases["nested if with correct getter"], +); +test( + `${ruleName}: ✅ Edge - if else if else with correct getter in all branches`, + edgeCases["if else if else with correct getter in all branches"], +); +test( + `${ruleName}: ❌ Edge - if else if else with wrong getter in else if`, + edgeCases["if else if else with wrong getter in else if"], +); +test( + `${ruleName}: ✅ Edge - if else if with final return correct getter in all paths`, + edgeCases["if else if with final return correct getter in all paths"], +); +test( + `${ruleName}: ❌ Edge - if else if with final return wrong getter in final return`, + edgeCases["if else if with final return wrong getter in final return"], +); diff --git a/packages/lint/src/tests/actor-liked-property-required.test.ts b/packages/lint/src/tests/actor-liked-property-required.test.ts index 825cee229..41a150f8c 100644 --- a/packages/lint/src/tests/actor-liked-property-required.test.ts +++ b/packages/lint/src/tests/actor-liked-property-required.test.ts @@ -79,3 +79,39 @@ test( `${ruleName}: ✅ Edge - nested ternary with property`, edgeCases["nested ternary with property"], ); +test( + `${ruleName}: ✅ Edge - if/else with property in both branches`, + edgeCases["if else with property in both branches"], +); +test( + `${ruleName}: ❌ Edge - if/else missing property in if block`, + edgeCases["if else missing property in if block"], +); +test( + `${ruleName}: ❌ Edge - if/else missing property in else block`, + edgeCases["if else missing property in else block"], +); +test( + `${ruleName}: ❌ Edge - if/else missing property in both blocks`, + edgeCases["if else missing property in both blocks"], +); +test( + `${ruleName}: ✅ Edge - nested if with property`, + edgeCases["nested if with property"], +); +test( + `${ruleName}: ✅ Edge - if else if else with liked in all branches`, + edgeCases["if else if else with property in all branches"], +); +test( + `${ruleName}: ❌ Edge - if else if else missing liked in else if`, + edgeCases["if else if else missing property in else if"], +); +test( + `${ruleName}: ✅ Edge - if else if with final return liked in all paths`, + edgeCases["if else if with final return property in all paths"], +); +test( + `${ruleName}: ❌ Edge - if else if with final return missing liked in final return`, + edgeCases["if else if with final return missing property in final return"], +); diff --git a/packages/lint/src/tests/actor-outbox-property-mismatch.test.ts b/packages/lint/src/tests/actor-outbox-property-mismatch.test.ts index 54e9d317a..5f1c8a38f 100644 --- a/packages/lint/src/tests/actor-outbox-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-outbox-property-mismatch.test.ts @@ -49,3 +49,39 @@ test( `${ruleName}: ✅ Edge - nested ternary with correct getter`, edgeCases["nested ternary with correct getter"], ); +test( + `${ruleName}: ✅ Edge - if/else with correct getter in both branches`, + edgeCases["if else with correct getter in both branches"], +); +test( + `${ruleName}: ❌ Edge - if/else with wrong getter in if block`, + edgeCases["if else with wrong getter in if block"], +); +test( + `${ruleName}: ❌ Edge - if/else with wrong getter in else block`, + edgeCases["if else with wrong getter in else block"], +); +test( + `${ruleName}: ❌ Edge - if/else with wrong getter in both blocks`, + edgeCases["if else with wrong getter in both blocks"], +); +test( + `${ruleName}: ✅ Edge - nested if with correct getter`, + edgeCases["nested if with correct getter"], +); +test( + `${ruleName}: ✅ Edge - if else if else with correct getter in all branches`, + edgeCases["if else if else with correct getter in all branches"], +); +test( + `${ruleName}: ❌ Edge - if else if else with wrong getter in else if`, + edgeCases["if else if else with wrong getter in else if"], +); +test( + `${ruleName}: ✅ Edge - if else if with final return correct getter in all paths`, + edgeCases["if else if with final return correct getter in all paths"], +); +test( + `${ruleName}: ❌ Edge - if else if with final return wrong getter in final return`, + edgeCases["if else if with final return wrong getter in final return"], +); diff --git a/packages/lint/src/tests/actor-outbox-property-required.test.ts b/packages/lint/src/tests/actor-outbox-property-required.test.ts index 1e44d9a80..b1945c7e8 100644 --- a/packages/lint/src/tests/actor-outbox-property-required.test.ts +++ b/packages/lint/src/tests/actor-outbox-property-required.test.ts @@ -79,3 +79,39 @@ test( `${ruleName}: ✅ Edge - nested ternary with property`, edgeCases["nested ternary with property"], ); +test( + `${ruleName}: ✅ Edge - if/else with property in both branches`, + edgeCases["if else with property in both branches"], +); +test( + `${ruleName}: ❌ Edge - if/else missing property in if block`, + edgeCases["if else missing property in if block"], +); +test( + `${ruleName}: ❌ Edge - if/else missing property in else block`, + edgeCases["if else missing property in else block"], +); +test( + `${ruleName}: ❌ Edge - if/else missing property in both blocks`, + edgeCases["if else missing property in both blocks"], +); +test( + `${ruleName}: ✅ Edge - nested if with property`, + edgeCases["nested if with property"], +); +test( + `${ruleName}: ✅ Edge - if else if else with outbox in all branches`, + edgeCases["if else if else with property in all branches"], +); +test( + `${ruleName}: ❌ Edge - if else if else missing outbox in else if`, + edgeCases["if else if else missing property in else if"], +); +test( + `${ruleName}: ✅ Edge - if else if with final return outbox in all paths`, + edgeCases["if else if with final return property in all paths"], +); +test( + `${ruleName}: ❌ Edge - if else if with final return missing outbox in final return`, + edgeCases["if else if with final return missing property in final return"], +); diff --git a/packages/lint/src/tests/actor-public-key-required.test.ts b/packages/lint/src/tests/actor-public-key-required.test.ts index fd44c6c62..5cae2f495 100644 --- a/packages/lint/src/tests/actor-public-key-required.test.ts +++ b/packages/lint/src/tests/actor-public-key-required.test.ts @@ -1,6 +1,7 @@ import { test } from "node:test"; import { properties, RULE_IDS } from "../lib/const.ts"; import { actorPropertyRequired } from "../lib/messages.ts"; +import { createKeyRequiredEdgeCaseTests } from "../lib/test-templates.ts"; import lintTest from "../lib/test.ts"; import * as rule from "../rules/actor-public-key-required.ts"; @@ -192,3 +193,63 @@ test( expectedError: actorPropertyRequired(properties.publicKey), }), ); + +// Edge case tests +const config = { rule, ruleName }; +const edgeCases = createKeyRequiredEdgeCaseTests("publicKey", config); +test( + `${ruleName}: ✅ Edge - ternary with property in both branches`, + edgeCases["ternary with property in both branches"], +); +test( + `${ruleName}: ❌ Edge - ternary missing property in consequent`, + edgeCases["ternary missing property in consequent"], +); +test( + `${ruleName}: ❌ Edge - ternary missing property in alternate`, + edgeCases["ternary missing property in alternate"], +); +test( + `${ruleName}: ❌ Edge - ternary missing property in both branches`, + edgeCases["ternary missing property in both branches"], +); +test( + `${ruleName}: ✅ Edge - nested ternary with property`, + edgeCases["nested ternary with property"], +); +test( + `${ruleName}: ✅ Edge - if/else with property in both branches`, + edgeCases["if else with property in both branches"], +); +test( + `${ruleName}: ❌ Edge - if/else missing property in if block`, + edgeCases["if else missing property in if block"], +); +test( + `${ruleName}: ❌ Edge - if/else missing property in else block`, + edgeCases["if else missing property in else block"], +); +test( + `${ruleName}: ❌ Edge - if/else missing property in both blocks`, + edgeCases["if else missing property in both blocks"], +); +test( + `${ruleName}: ✅ Edge - nested if with property`, + edgeCases["nested if with property"], +); +test( + `${ruleName}: ✅ Edge - if else if else with property in all branches`, + edgeCases["if else if else with property in all branches"], +); +test( + `${ruleName}: ❌ Edge - if else if else missing property in else if`, + edgeCases["if else if else missing property in else if"], +); +test( + `${ruleName}: ✅ Edge - if else if with final return property in all paths`, + edgeCases["if else if with final return property in all paths"], +); +test( + `${ruleName}: ❌ Edge - if else if with final return missing property in final return`, + edgeCases["if else if with final return missing property in final return"], +); diff --git a/packages/lint/src/tests/actor-shared-inbox-property-mismatch.test.ts b/packages/lint/src/tests/actor-shared-inbox-property-mismatch.test.ts index dc4d4c484..6d9c2d153 100644 --- a/packages/lint/src/tests/actor-shared-inbox-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-shared-inbox-property-mismatch.test.ts @@ -49,3 +49,39 @@ test( `${ruleName}: ✅ Edge - nested ternary with correct getter`, edgeCases["nested ternary with correct getter"], ); +test( + `${ruleName}: ✅ Edge - if/else with correct getter in both branches`, + edgeCases["if else with correct getter in both branches"], +); +test( + `${ruleName}: ❌ Edge - if/else with wrong getter in if block`, + edgeCases["if else with wrong getter in if block"], +); +test( + `${ruleName}: ❌ Edge - if/else with wrong getter in else block`, + edgeCases["if else with wrong getter in else block"], +); +test( + `${ruleName}: ❌ Edge - if/else with wrong getter in both blocks`, + edgeCases["if else with wrong getter in both blocks"], +); +test( + `${ruleName}: ✅ Edge - nested if with correct getter`, + edgeCases["nested if with correct getter"], +); +test( + `${ruleName}: ✅ Edge - if else if else with correct getter in all branches`, + edgeCases["if else if else with correct getter in all branches"], +); +test( + `${ruleName}: ❌ Edge - if else if else with wrong getter in else if`, + edgeCases["if else if else with wrong getter in else if"], +); +test( + `${ruleName}: ✅ Edge - if else if with final return correct getter in all paths`, + edgeCases["if else if with final return correct getter in all paths"], +); +test( + `${ruleName}: ❌ Edge - if else if with final return wrong getter in final return`, + edgeCases["if else if with final return wrong getter in final return"], +); diff --git a/packages/lint/src/tests/actor-shared-inbox-property-required.test.ts b/packages/lint/src/tests/actor-shared-inbox-property-required.test.ts index 3ad58e56f..b332b1bd1 100644 --- a/packages/lint/src/tests/actor-shared-inbox-property-required.test.ts +++ b/packages/lint/src/tests/actor-shared-inbox-property-required.test.ts @@ -79,3 +79,39 @@ test( `${ruleName}: ✅ Edge - nested ternary with property`, edgeCases["nested ternary with property"], ); +test( + `${ruleName}: ✅ Edge - if/else with property in both branches`, + edgeCases["if else with property in both branches"], +); +test( + `${ruleName}: ❌ Edge - if/else missing property in if block`, + edgeCases["if else missing property in if block"], +); +test( + `${ruleName}: ❌ Edge - if/else missing property in else block`, + edgeCases["if else missing property in else block"], +); +test( + `${ruleName}: ❌ Edge - if/else missing property in both blocks`, + edgeCases["if else missing property in both blocks"], +); +test( + `${ruleName}: ✅ Edge - nested if with property`, + edgeCases["nested if with property"], +); +test( + `${ruleName}: ✅ Edge - if else if else with sharedInbox in all branches`, + edgeCases["if else if else with property in all branches"], +); +test( + `${ruleName}: ❌ Edge - if else if else missing sharedInbox in else if`, + edgeCases["if else if else missing property in else if"], +); +test( + `${ruleName}: ✅ Edge - if else if with final return sharedInbox in all paths`, + edgeCases["if else if with final return property in all paths"], +); +test( + `${ruleName}: ❌ Edge - if else if with final return missing sharedInbox in final return`, + edgeCases["if else if with final return missing property in final return"], +); From 07642800c70bdd0efd26be7e790058b4052db117 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Tue, 16 Dec 2025 11:57:47 +0000 Subject: [PATCH 34/41] Refactor tests to use `runTests` utility for consistency and maintainability --- packages/lint/src/lib/test-templates.ts | 2449 +++++++++-------- .../actor-assertion-method-required.test.ts | 270 +- .../actor-featured-property-mismatch.test.ts | 80 +- .../actor-featured-property-required.test.ts | 111 +- ...or-featured-tags-property-mismatch.test.ts | 80 +- ...or-featured-tags-property-required.test.ts | 114 +- .../actor-followers-property-mismatch.test.ts | 80 +- .../actor-followers-property-required.test.ts | 111 +- .../actor-following-property-mismatch.test.ts | 80 +- .../actor-following-property-required.test.ts | 111 +- .../lint/src/tests/actor-id-mismatch.test.ts | 89 +- .../lint/src/tests/actor-id-required.test.ts | 94 +- .../actor-inbox-property-mismatch.test.ts | 80 +- .../actor-inbox-property-required.test.ts | 110 +- .../actor-liked-property-mismatch.test.ts | 80 +- .../actor-liked-property-required.test.ts | 111 +- .../actor-outbox-property-mismatch.test.ts | 80 +- .../actor-outbox-property-required.test.ts | 111 +- .../tests/actor-public-key-required.test.ts | 260 +- ...tor-shared-inbox-property-mismatch.test.ts | 80 +- ...tor-shared-inbox-property-required.test.ts | 110 +- 21 files changed, 1342 insertions(+), 3349 deletions(-) diff --git a/packages/lint/src/lib/test-templates.ts b/packages/lint/src/lib/test-templates.ts index 9682c7a7a..f2ac71242 100644 --- a/packages/lint/src/lib/test-templates.ts +++ b/packages/lint/src/lib/test-templates.ts @@ -1,4 +1,6 @@ +import { consume, entries, map, pipe } from "@fxts/core"; import type { Rule } from "eslint"; +import { test } from "node:test"; import { properties } from "./const.ts"; import { actorPropertyMismatch, actorPropertyRequired } from "./messages.ts"; import lintTest from "./test.ts"; @@ -14,6 +16,27 @@ interface TestConfig { ruleName: string; } +type TestEntry = [() => void, boolean]; +type TestSuite = Record; + +const ID_PROP = "id: ctx.getActorUri(identifier),"; + +/** + * Runs all tests in a test suite with proper formatting + */ +export const runTests = (ruleName: string, tests: TestSuite) => + pipe( + tests, + entries, + map(([testName, [testFn, succeed]]) => { + test( + `${ruleName}: ${succeed ? "✅ Good" : "❌ Bad"} - ${testName}`, + testFn, + ); + }), + consume, + ); + const createDispatcherCode = (content: string) => ` federation.setActorDispatcher( "/users/{identifier}", @@ -27,8 +50,10 @@ const createChainedDispatcherCode = ( dispatcherMethod: string, ) => ` federation - .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - ${content} + .setActorDispatcher( + "/users/{identifier}", + async (ctx, identifier) => { + ${content} }) .${dispatcherMethod}(async (ctx, identifier) => []); `; @@ -67,7 +92,7 @@ const createMethodCall = ( * Handles both simple properties and nested properties with wrappers. * * @example - * // Simple property: id: ctx.getActorUri(identifier), + * // Simple property: id: ctx.getActorUri(identifier) * // Nested property: * // endpoints: new Endpoints({ sharedInbox: ctx.getInboxUri() }), */ @@ -119,14 +144,9 @@ const createTestCode = ( ) => { const prop = properties[propertyKey]; - let propertyCode = ""; - if (includeProperty) { - propertyCode = createPropertyAssignment(prop); - } - return `return new Person({ - id: ctx.getActorUri(identifier), - ${propertyCode} + ${ID_PROP} + ${includeProperty ? createPropertyAssignment(prop) : ""} name: "John Doe", });`; }; @@ -142,482 +162,264 @@ const createTestCode = ( export function createRequiredDispatcherRuleTests( propertyKey: PropertyKey, config: TestConfig, -) { +): TestSuite { const { rule, ruleName } = config; const prop = properties[propertyKey]; const expectedError = actorPropertyRequired(prop); return { // ✅ Good - non-Federation object - "non-federation object": lintTest({ - code: createDispatcherCode(createTestCode(propertyKey, false)), - rule, - ruleName, - federationSetup: ` + "non-federation object": [ + lintTest({ + code: createDispatcherCode(createTestCode(propertyKey, false)), + rule, + ruleName, + federationSetup: ` const federation = { setActorDispatcher: () => {} }; `, - }), + }), + true, + ], // ✅ Good - dispatcher NOT configured - "dispatcher not configured": lintTest({ - code: createDispatcherCode(createTestCode(propertyKey, false)), - rule, - ruleName, - }), + "dispatcher not configured": [ + lintTest({ + code: createDispatcherCode(createTestCode(propertyKey, false)), + rule, + ruleName, + }), + true, + ], // ✅ Good - dispatcher configured BEFORE (chained) - "dispatcher before chained with property": lintTest({ - code: createChainedDispatcherCode( - createTestCode(propertyKey, true), - prop.setter, - ), - rule, - ruleName, - }), + "dispatcher before chained with property": [ + lintTest({ + code: createChainedDispatcherCode( + createTestCode(propertyKey, true), + prop.setter, + ), + rule, + ruleName, + }), + true, + ], // ✅ Good - dispatcher configured BEFORE (separate) - "dispatcher before separate with property": lintTest({ - code: createSeparateDispatcherCode( - createTestCode(propertyKey, true), - prop.setter, - true, - ), - rule, - ruleName, - }), + "dispatcher before separate with property": [ + lintTest({ + code: createSeparateDispatcherCode( + createTestCode(propertyKey, true), + prop.setter, + true, + ), + rule, + ruleName, + }), + true, + ], // ✅ Good - dispatcher configured AFTER (chained) - "dispatcher after chained with property": lintTest({ - code: createChainedDispatcherCode( - createTestCode(propertyKey, true), - prop.setter, - ), - rule, - ruleName, - }), + "dispatcher after chained with property": [ + lintTest({ + code: createChainedDispatcherCode( + createTestCode(propertyKey, true), + prop.setter, + ), + rule, + ruleName, + }), + true, + ], // ✅ Good - dispatcher configured AFTER (separate) - "dispatcher after separate with property": lintTest({ - code: createSeparateDispatcherCode( - createTestCode(propertyKey, true), - prop.setter, - false, - ), - rule, - ruleName, - }), + "dispatcher after separate with property": [ + lintTest({ + code: createSeparateDispatcherCode( + createTestCode(propertyKey, true), + prop.setter, + false, + ), + rule, + ruleName, + }), + true, + ], // ❌ Bad - dispatcher configured, property missing - "dispatcher configured property missing": lintTest({ - code: createChainedDispatcherCode( - createTestCode(propertyKey, false), - prop.setter, - ), - rule, - ruleName, - expectedError, - }), + "dispatcher configured property missing": [ + lintTest({ + code: createChainedDispatcherCode( + createTestCode(propertyKey, false), + prop.setter, + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ❌ Bad - dispatcher before (separate), property missing - "dispatcher before separate property missing": lintTest({ - code: createSeparateDispatcherCode( - createTestCode(propertyKey, false), - prop.setter, - true, - ), - rule, - ruleName, - expectedError, - }), + "dispatcher before separate property missing": [ + lintTest({ + code: createSeparateDispatcherCode( + createTestCode(propertyKey, false), + prop.setter, + true, + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ❌ Bad - dispatcher after (separate), property missing - "dispatcher after separate property missing": lintTest({ - code: createSeparateDispatcherCode( - createTestCode(propertyKey, false), - prop.setter, - false, - ), - rule, - ruleName, - expectedError, - }), + "dispatcher after separate property missing": [ + lintTest({ + code: createSeparateDispatcherCode( + createTestCode(propertyKey, false), + prop.setter, + false, + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ❌ Bad - variable assignment without property - "variable assignment without property": lintTest({ - code: createChainedDispatcherCode( - `const actor = new Person({ - id: ctx.getActorUri(identifier), + "variable assignment without property": [ + lintTest({ + code: createChainedDispatcherCode( + `const actor = new Person({ + ${ID_PROP} name: "John Doe", }); return actor;`, - prop.setter, - ), - rule, - ruleName, - expectedError, - }), - }; -} - -/** - * Creates required rule tests for inbox/sharedInbox properties - */ -export function createRequiredListenerRuleTests( - propertyKey: "inbox" | "sharedInbox", - config: TestConfig, -) { - const { rule, ruleName } = config; - const prop = properties[propertyKey]; - const expectedError = actorPropertyRequired(prop); - - const createLocalPropertyCode = (include: boolean) => { - if (!include) return ""; - return createPropertyAssignment(prop); - }; - - const createActorCode = (includeProperty: boolean) => { - const propCode = createLocalPropertyCode(includeProperty); - return `return new Person({ - id: ctx.getActorUri(identifier), - ${propCode} - name: "John Doe", - });`; - }; - - return { - // ✅ Good - non-Federation object - "non-federation object": lintTest({ - code: createDispatcherCode(createActorCode(false)), - rule, - ruleName, - federationSetup: ` - const federation = { setActorDispatcher: () => {} }; - `, - }), - - // ✅ Good - listeners NOT configured - "listeners not configured": lintTest({ - code: createDispatcherCode(createActorCode(false)), - rule, - ruleName, - }), - - // ✅ Good - listeners configured BEFORE (chained) - "listeners before chained with property": lintTest({ - code: createChainedDispatcherCode( - createActorCode(true), - "setInboxListeners", - ), - rule, - ruleName, - }), - - // ✅ Good - listeners configured BEFORE (separate) - "listeners before separate with property": lintTest({ - code: createSeparateDispatcherCode( - createActorCode(true), - "setInboxListeners", - true, - ), - rule, - ruleName, - }), - - // ✅ Good - listeners configured AFTER (chained) - "listeners after chained with property": lintTest({ - code: createChainedDispatcherCode( - createActorCode(true), - "setInboxListeners", - ), - rule, - ruleName, - }), - - // ✅ Good - listeners configured AFTER (separate) - "listeners after separate with property": lintTest({ - code: createSeparateDispatcherCode( - createActorCode(true), - "setInboxListeners", - false, - ), - rule, - ruleName, - }), - - // ❌ Bad - listeners configured, property missing - "listeners configured property missing": lintTest({ - code: createChainedDispatcherCode( - createActorCode(false), - "setInboxListeners", - ), - rule, - ruleName, - expectedError, - }), - - // ❌ Bad - listeners before (separate), property missing - "listeners before separate property missing": lintTest({ - code: createSeparateDispatcherCode( - createActorCode(false), - "setInboxListeners", - true, - ), - rule, - ruleName, - expectedError, - }), - - // ❌ Bad - listeners after (separate), property missing - "listeners after separate property missing": lintTest({ - code: createSeparateDispatcherCode( - createActorCode(false), - "setInboxListeners", - false, - ), - rule, - ruleName, - expectedError, - }), - - // ❌ Bad - variable assignment without property - "variable assignment without property": lintTest({ - code: createChainedDispatcherCode( - `const actor = new Person({ - id: ctx.getActorUri(identifier), - name: "John Doe", - }); - return actor;`, - "setInboxListeners", - ), - rule, - ruleName, - expectedError, - }), + prop.setter, + ), + rule, + ruleName, + expectedError, + }), + false, + ], }; } /** * Creates required rule tests for actor id property (no dispatcher needed) */ -export function createIdRequiredRuleTests(config: TestConfig) { +export function createIdRequiredRuleTests(config: TestConfig): TestSuite { const { rule, ruleName } = config; const expectedError = actorPropertyRequired(properties.id); return { // ✅ Good - non-Federation object - "non-federation object": lintTest({ - code: createDispatcherCode(`return new Person({ name: "John Doe" });`), - rule, - ruleName, - federationSetup: ` + "non-federation object": [ + lintTest({ + code: createDispatcherCode(`return new Person({ name: "John Doe" });`), + rule, + ruleName, + federationSetup: ` const federation = { setActorDispatcher: () => {} }; `, - }), + }), + true, + ], // ✅ Good - with id property (any value) - "with id property any value": lintTest({ - code: createDispatcherCode(`return new Person({ + "with id property any value": [ + lintTest({ + code: createDispatcherCode(`return new Person({ id: "https://example.com/users/123", name: "John Doe", });`), - rule, - ruleName, - }), + rule, + ruleName, + }), + true, + ], // ✅ Good - with id property using ctx.getActorUri() - "with id property using getActorUri": lintTest({ - code: createDispatcherCode(`return new Person({ - id: ctx.getActorUri(identifier), + "with id property using getActorUri": [ + lintTest({ + code: createDispatcherCode(`return new Person({ + ${ID_PROP} name: "John Doe", });`), - rule, - ruleName, - }), + rule, + ruleName, + }), + true, + ], // ✅ Good - BlockStatement with id - "block statement with id": lintTest({ - code: createDispatcherCode(`const name = "John Doe"; + "block statement with id": [ + lintTest({ + code: createDispatcherCode(`const name = "John Doe"; return new Person({ - id: ctx.getActorUri(identifier), + ${ID_PROP} name, });`), - rule, - ruleName, - }), + rule, + ruleName, + }), + true, + ], // ❌ Bad - without id property - "without id property": lintTest({ - code: createDispatcherCode(`return new Person({ name: "John Doe" });`), - rule, - ruleName, - expectedError, - }), + "without id property": [ + lintTest({ + code: createDispatcherCode(`return new Person({ name: "John Doe" });`), + rule, + ruleName, + expectedError, + }), + false, + ], // ❌ Bad - returning empty object - "returning empty object": lintTest({ - code: createDispatcherCode(`return new Person({});`), - rule, - ruleName, - expectedError, - }), + "returning empty object": [ + lintTest({ + code: createDispatcherCode(`return new Person({});`), + rule, + ruleName, + expectedError, + }), + false, + ], // ✅ Good - multiple properties including id - "multiple properties including id": lintTest({ - code: createDispatcherCode(`return new Person({ - id: ctx.getActorUri(identifier), + "multiple properties including id": [ + lintTest({ + code: createDispatcherCode(`return new Person({ + ${ID_PROP} name: "John Doe", inbox: ctx.getInboxUri(identifier), outbox: ctx.getOutboxUri(identifier), });`), - rule, - ruleName, - }), + rule, + ruleName, + }), + true, + ], // ❌ Bad - variable assignment without id - "variable assignment without id": lintTest({ - code: createDispatcherCode( - `const actor = new Person({ name: "John Doe" }); + "variable assignment without id": [ + lintTest({ + code: createDispatcherCode( + `const actor = new Person({ name: "John Doe" }); return actor;`, - ), - rule, - ruleName, - expectedError, - }), - }; -} - -/** - * Creates required rule tests for key-related properties - * (publicKey, assertionMethod) - */ -export function createKeyRequiredRuleTests( - propertyName: "publicKey" | "assertionMethod", - config: TestConfig, -) { - const { rule, ruleName } = config; - const prop = properties[propertyName]; - const expectedError = actorPropertyRequired(prop); - - const createActorWithKey = (includeProperty: boolean) => { - const propCode = includeProperty - ? `${propertyName}: ctx.getActorKeyPairs(identifier),` - : ""; - return `return new Person({ - id: ctx.getActorUri(identifier), - ${propCode} - name: "John Doe", - });`; - }; - - return { - // ✅ Good - non-Federation object - "non-federation object": lintTest({ - code: createDispatcherCode(`return new Person({ name: "John Doe" });`), - rule, - ruleName, - federationSetup: ` - const federation = { setActorDispatcher: () => {} }; - `, - }), - - // ✅ Good - key pairs dispatcher NOT configured - "key pairs dispatcher not configured": lintTest({ - code: createDispatcherCode(createActorWithKey(false)), - rule, - ruleName, - }), - - // ✅ Good - key pairs dispatcher configured BEFORE (separate) - "key pairs before separate with property": lintTest({ - code: createSeparateDispatcherCode( - createActorWithKey(true), - "setKeyPairsDispatcher", - true, - ), - rule, - ruleName, - }), - - // ✅ Good - key pairs dispatcher configured BEFORE (chained) - "key pairs before chained with property": lintTest({ - code: createChainedDispatcherCode( - createActorWithKey(true), - "setKeyPairsDispatcher", - ), - rule, - ruleName, - }), - - // ✅ Good - key pairs dispatcher configured AFTER (separate) - "key pairs after separate with property": lintTest({ - code: createSeparateDispatcherCode( - createActorWithKey(true), - "setKeyPairsDispatcher", - false, - ), - rule, - ruleName, - }), - - // ✅ Good - key pairs dispatcher configured AFTER (chained) - "key pairs after chained with property": lintTest({ - code: createChainedDispatcherCode( - createActorWithKey(true), - "setKeyPairsDispatcher", - ), - rule, - ruleName, - }), - - // ❌ Bad - key pairs dispatcher configured BEFORE (separate), - // property missing - "key pairs before separate property missing": lintTest({ - code: createSeparateDispatcherCode( - createActorWithKey(false), - "setKeyPairsDispatcher", - true, - ), - rule, - ruleName, - expectedError, - }), - - // ❌ Bad - key pairs dispatcher configured BEFORE (chained), - // property missing - "key pairs before chained property missing": lintTest({ - code: createChainedDispatcherCode( - createActorWithKey(false), - "setKeyPairsDispatcher", - ), - rule, - ruleName, - expectedError, - }), - - // ❌ Bad - key pairs dispatcher configured AFTER (separate), - // property missing - "key pairs after separate property missing": lintTest({ - code: createSeparateDispatcherCode( - createActorWithKey(false), - "setKeyPairsDispatcher", - false, - ), - rule, - ruleName, - expectedError, - }), - - // ❌ Bad - key pairs dispatcher configured AFTER (chained), - // property missing - "key pairs after chained property missing": lintTest({ - code: createChainedDispatcherCode( - createActorWithKey(false), - "setKeyPairsDispatcher", - ), - rule, - ruleName, - expectedError, - }), + ), + rule, + ruleName, + expectedError, + }), + false, + ], }; } @@ -631,7 +433,7 @@ export function createKeyRequiredRuleTests( export function createMismatchRuleTests( propertyKey: PropertyKey, config: TestConfig, -) { +): TestSuite { const { rule, ruleName } = config; const prop = properties[propertyKey]; @@ -649,65 +451,80 @@ export function createMismatchRuleTests( const createActorCode = (getter: string) => `return new Person({ - id: ctx.getActorUri(identifier), + ${ID_PROP} ${createLocalPropertyCode(getter)} name: "John Doe", });`; return { // ✅ Good - non-Federation object - "non-federation object": lintTest({ - code: createDispatcherCode(createActorCode(wrongGetter)), - rule, - ruleName, - federationSetup: ` + "non-federation object": [ + lintTest({ + code: createDispatcherCode(createActorCode(wrongGetter)), + rule, + ruleName, + federationSetup: ` const federation = { setActorDispatcher: () => {} }; `, - }), + }), + true, + ], // ✅ Good - correct getter used - "correct getter used": lintTest({ - code: createDispatcherCode(createActorCode(prop.getter)), - rule, - ruleName, - }), + "correct getter used": [ + lintTest({ + code: createDispatcherCode(createActorCode(prop.getter)), + rule, + ruleName, + }), + true, + ], // ❌ Bad - wrong getter used - "wrong getter used": lintTest({ - code: createDispatcherCode(createActorCode(wrongGetter)), - rule, - ruleName, - expectedError, - }), + "wrong getter used": [ + lintTest({ + code: createDispatcherCode(createActorCode(wrongGetter)), + rule, + ruleName, + expectedError, + }), + false, + ], // ❌ Bad - wrong identifier - "wrong identifier": lintTest({ - code: createDispatcherCode(`return new Person({ - id: ctx.getActorUri(identifier), + "wrong identifier": [ + lintTest({ + code: createDispatcherCode(`return new Person({ + ${ID_PROP} ${createPropertyAssignment(prop, { ctxName: "wrongContext" })} name: "John Doe", });`), - rule, - ruleName, - expectedError, - }), + rule, + ruleName, + expectedError, + }), + false, + ], // ✅ Good - property not present (no error) - "property not present": lintTest({ - code: createDispatcherCode(`return new Person({ - id: ctx.getActorUri(identifier), + "property not present": [ + lintTest({ + code: createDispatcherCode(`return new Person({ + ${ID_PROP} name: "John Doe", });`), - rule, - ruleName, - }), + rule, + ruleName, + }), + true, + ], }; } /** * Creates mismatch rule tests for id property */ -export function createIdMismatchRuleTests(config: TestConfig) { +export function createIdMismatchRuleTests(config: TestConfig): TestSuite { const { rule, ruleName } = config; const expectedError = actorPropertyMismatch( createMethodCallContext(properties.id), @@ -715,65 +532,83 @@ export function createIdMismatchRuleTests(config: TestConfig) { return { // ✅ Good - non-Federation object - "non-federation object": lintTest({ - code: createDispatcherCode( - `return new Person({ id: ctx.getFollowingUri(identifier) });`, - ), - rule, - ruleName, - federationSetup: ` + "non-federation object": [ + lintTest({ + code: createDispatcherCode( + `return new Person({ id: ctx.getFollowingUri(identifier) });`, + ), + rule, + ruleName, + federationSetup: ` const federation = { setActorDispatcher: () => {} }; `, - }), + }), + true, + ], // ✅ Good - correct getter used - "correct getter used": lintTest({ - code: createDispatcherCode( - `return new Person({ id: ctx.getActorUri(identifier) });`, - ), - rule, - ruleName, - }), + "correct getter used": [ + lintTest({ + code: createDispatcherCode( + `return new Person({ id: ctx.getActorUri(identifier) });`, + ), + rule, + ruleName, + }), + true, + ], // ❌ Bad - literal string id - "literal string id": lintTest({ - code: createDispatcherCode( - `return new Person({ id: "https://example.com/users/123" });`, - ), - rule, - ruleName, - expectedError, - }), + "literal string id": [ + lintTest({ + code: createDispatcherCode( + `return new Person({ id: "https://example.com/users/123" });`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ❌ Bad - new URL as id - "new URL as id": lintTest({ - code: createDispatcherCode( - `return new Person({ id: new URL("https://example.com/users/123") });`, - ), - rule, - ruleName, - expectedError, - }), + "new URL as id": [ + lintTest({ + code: createDispatcherCode( + `return new Person({ id: new URL("https://example.com/users/123") });`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ❌ Bad - wrong getter used - "wrong getter used": lintTest({ - code: createDispatcherCode( - `return new Person({ id: ctx.getFollowingUri(identifier) });`, - ), - rule, - ruleName, - expectedError, - }), + "wrong getter used": [ + lintTest({ + code: createDispatcherCode( + `return new Person({ id: ctx.getFollowingUri(identifier) });`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ❌ Bad - wrong identifier - "wrong identifier": lintTest({ - code: createDispatcherCode( - `return new Person({ id: wrongContext.getActorUri(identifier) });`, - ), - rule, - ruleName, - expectedError, - }), + "wrong identifier": [ + lintTest({ + code: createDispatcherCode( + `return new Person({ id: wrongContext.getActorUri(identifier) });`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], }; } @@ -787,10 +622,10 @@ export function createIdMismatchRuleTests(config: TestConfig) { export function createRequiredEdgeCaseTests( propertyKey: PropertyKey, config: TestConfig, - dispatcherMethod: string, -) { +): TestSuite { const { rule, ruleName } = config; const prop = properties[propertyKey]; + const setter = prop.setter; const expectedError = actorPropertyRequired(prop); const createLocalPropertyCode = () => createPropertyAssignment(prop); @@ -798,224 +633,266 @@ export function createRequiredEdgeCaseTests( return { // ✅ Ternary with property in both branches - "ternary with property in both branches": lintTest({ - code: createChainedDispatcherCode( - `\ + "ternary with property in both branches": [ + lintTest({ + code: createChainedDispatcherCode( + `\ return condition - ? new Person({ id: ctx.getActorUri(identifier), \ + ? new Person({ ${ID_PROP} \ ${propCode} name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" });`, - dispatcherMethod, - ), - rule, - ruleName, - }), + : new Person({ ${ID_PROP} ${propCode} name: "B" });`, + setter, + ), + rule, + ruleName, + }), + true, + ], // ❌ Ternary missing property in consequent - "ternary missing property in consequent": lintTest({ - code: createChainedDispatcherCode( - `\ + "ternary missing property in consequent": [ + lintTest({ + code: createChainedDispatcherCode( + `\ return condition - ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" });`, - dispatcherMethod, - ), - rule, - ruleName, - expectedError, - }), + ? new Person({ ${ID_PROP} name: "A" }) + : new Person({ ${ID_PROP} ${propCode} name: "B" });`, + setter, + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ❌ Ternary missing property in alternate - "ternary missing property in alternate": lintTest({ - code: createChainedDispatcherCode( - `\ + "ternary missing property in alternate": [ + lintTest({ + code: createChainedDispatcherCode( + `\ return condition - ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), name: "B" });`, - dispatcherMethod, - ), - rule, - ruleName, - expectedError, - }), + ? new Person({ ${ID_PROP} ${propCode} name: "A" }) + : new Person({ ${ID_PROP} name: "B" });`, + setter, + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ❌ Ternary missing property in both - "ternary missing property in both branches": lintTest({ - code: createChainedDispatcherCode( - `return condition - ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), name: "B" });`, - dispatcherMethod, - ), - rule, - ruleName, - expectedError, - }), + "ternary missing property in both branches": [ + lintTest({ + code: createChainedDispatcherCode( + `return condition + ? new Person({ ${ID_PROP} name: "A" }) + : new Person({ ${ID_PROP} name: "B" });`, + setter, + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ✅ Nested ternary with property - "nested ternary with property": lintTest({ - code: createChainedDispatcherCode( - `\ + "nested ternary with property": [ + lintTest({ + code: createChainedDispatcherCode( + `\ return condition1 ? (condition2 - ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" })) - : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" });`, - dispatcherMethod, - ), - rule, - ruleName, - }), + ? new Person({ ${ID_PROP} ${propCode} name: "A" }) + : new Person({ ${ID_PROP} ${propCode} name: "B" })) + : new Person({ ${ID_PROP} ${propCode} name: "C" });`, + setter, + ), + rule, + ruleName, + }), + true, + ], // ✅ If/else with property in both branches - "if else with property in both branches": lintTest({ - code: createChainedDispatcherCode( - `\ + "if else with property in both branches": [ + lintTest({ + code: createChainedDispatcherCode( + `\ if (condition) { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); + return new Person({ ${ID_PROP} ${propCode} name: "A" }); } else { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); + return new Person({ ${ID_PROP} ${propCode} name: "B" }); }`, - dispatcherMethod, - ), - rule, - ruleName, - }), + setter, + ), + rule, + ruleName, + }), + true, + ], // ❌ If/else missing property in if block - "if else missing property in if block": lintTest({ - code: createChainedDispatcherCode( - `\ + "if else missing property in if block": [ + lintTest({ + code: createChainedDispatcherCode( + `\ if (condition) { - return new Person({ id: ctx.getActorUri(identifier), name: "A" }); + return new Person({ ${ID_PROP} name: "A" }); } else { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); + return new Person({ ${ID_PROP} ${propCode} name: "B" }); }`, - dispatcherMethod, - ), - rule, - ruleName, - expectedError, - }), + setter, + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ❌ If/else missing property in else block - "if else missing property in else block": lintTest({ - code: createChainedDispatcherCode( - `\ + "if else missing property in else block": [ + lintTest({ + code: createChainedDispatcherCode( + `\ if (condition) { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); + return new Person({ ${ID_PROP} ${propCode} name: "A" }); } else { - return new Person({ id: ctx.getActorUri(identifier), name: "B" }); + return new Person({ ${ID_PROP} name: "B" }); }`, - dispatcherMethod, - ), - rule, - ruleName, - expectedError, - }), + setter, + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ❌ If/else missing property in both blocks - "if else missing property in both blocks": lintTest({ - code: createChainedDispatcherCode( - `\ + "if else missing property in both blocks": [ + lintTest({ + code: createChainedDispatcherCode( + `\ if (condition) { - return new Person({ id: ctx.getActorUri(identifier), name: "A" }); + return new Person({ ${ID_PROP} name: "A" }); } else { - return new Person({ id: ctx.getActorUri(identifier), name: "B" }); + return new Person({ ${ID_PROP} name: "B" }); }`, - dispatcherMethod, - ), - rule, - ruleName, - expectedError, - }), + setter, + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ✅ Nested if with property - "nested if with property": lintTest({ - code: createChainedDispatcherCode( - `\ + "nested if with property": [ + lintTest({ + code: createChainedDispatcherCode( + `\ if (condition1) { if (condition2) { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); + return new Person({ ${ID_PROP} ${propCode} name: "A" }); } else { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); + return new Person({ ${ID_PROP} ${propCode} name: "B" }); } } else { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" }); + return new Person({ ${ID_PROP} ${propCode} name: "C" }); }`, - dispatcherMethod, - ), - rule, - ruleName, - }), + setter, + ), + rule, + ruleName, + }), + true, + ], // ✅ If/else if/else with property in all branches - "if else if else with property in all branches": lintTest({ - code: createChainedDispatcherCode( - `\ + "if else if else with property in all branches": [ + lintTest({ + code: createChainedDispatcherCode( + `\ if (condition1) { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); + return new Person({ ${ID_PROP} ${propCode} name: "A" }); } else if (condition2) { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); + return new Person({ ${ID_PROP} ${propCode} name: "B" }); } else { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" }); + return new Person({ ${ID_PROP} ${propCode} name: "C" }); }`, - dispatcherMethod, - ), - rule, - ruleName, - }), + setter, + ), + rule, + ruleName, + }), + true, + ], // ❌ If/else if/else missing property in else if - "if else if else missing property in else if": lintTest({ - code: createChainedDispatcherCode( - `\ + "if else if else missing property in else if": [ + lintTest({ + code: createChainedDispatcherCode( + `\ if (condition1) { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); + return new Person({ ${ID_PROP} ${propCode} name: "A" }); } else if (condition2) { - return new Person({ id: ctx.getActorUri(identifier), name: "B" }); + return new Person({ ${ID_PROP} name: "B" }); } else { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" }); + return new Person({ ${ID_PROP} ${propCode} name: "C" }); }`, - dispatcherMethod, - ), - rule, - ruleName, - expectedError, - }), + setter, + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ✅ If/else if with final return, property in all paths - "if else if with final return property in all paths": lintTest({ - code: createChainedDispatcherCode( - `\ + "if else if with final return property in all paths": [ + lintTest({ + code: createChainedDispatcherCode( + `\ if (condition1) { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); + return new Person({ ${ID_PROP} ${propCode} name: "A" }); } else if (condition2) { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); + return new Person({ ${ID_PROP} ${propCode} name: "B" }); } -return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" });`, - dispatcherMethod, - ), - rule, - ruleName, - }), +return new Person({ ${ID_PROP} ${propCode} name: "C" });`, + setter, + ), + rule, + ruleName, + }), + true, + ], // ❌ If/else if with final return, missing property in final return - "if else if with final return missing property in final return": lintTest({ - code: createChainedDispatcherCode( - `\ + "if else if with final return missing property in final return": [ + lintTest({ + code: createChainedDispatcherCode( + `\ if (condition1) { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); + return new Person({ ${ID_PROP} ${propCode} name: "A" }); } else if (condition2) { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); + return new Person({ ${ID_PROP} ${propCode} name: "B" }); } -return new Person({ id: ctx.getActorUri(identifier), name: "C" });`, - dispatcherMethod, - ), - rule, - ruleName, - expectedError, - }), +return new Person({ ${ID_PROP} name: "C" });`, + setter, + ), + rule, + ruleName, + expectedError, + }), + false, + ], }; } @@ -1025,7 +902,7 @@ return new Person({ id: ctx.getActorUri(identifier), name: "C" });`, export function createMismatchEdgeCaseTests( propertyKey: PropertyKey, config: TestConfig, -) { +): TestSuite { const { rule, ruleName } = config; const prop = properties[propertyKey]; @@ -1044,423 +921,499 @@ export function createMismatchEdgeCaseTests( const wrongPropCode = createLocalPropertyCode(wrongGetter); return { // ✅ Ternary with correct getter in both branches - "ternary with correct getter in both branches": lintTest({ - code: createDispatcherCode(nestedTernaryCode(propCode, propCode)), - rule, - ruleName, - }), + "ternary with correct getter in both branches": [ + lintTest({ + code: createDispatcherCode(nestedTernaryCode(propCode, propCode)), + rule, + ruleName, + }), + true, + ], // ❌ Ternary with wrong getter in consequent - "ternary with wrong getter in consequent": lintTest({ - code: createDispatcherCode(nestedTernaryCode(wrongPropCode, propCode)), - rule, - ruleName, - expectedError, - }), + "ternary with wrong getter in consequent": [ + lintTest({ + code: createDispatcherCode(nestedTernaryCode(wrongPropCode, propCode)), + rule, + ruleName, + expectedError, + }), + false, + ], // ❌ Ternary with wrong getter in alternate - "ternary with wrong getter in alternate": lintTest({ - code: createDispatcherCode(nestedTernaryCode(propCode, wrongPropCode)), - rule, - ruleName, - expectedError, - }), + "ternary with wrong getter in alternate": [ + lintTest({ + code: createDispatcherCode(nestedTernaryCode(propCode, wrongPropCode)), + rule, + ruleName, + expectedError, + }), + false, + ], // ❌ Ternary with wrong getter in both - "ternary with wrong getter in both branches": lintTest({ - code: createDispatcherCode( - nestedTernaryCode(wrongPropCode, wrongPropCode), - ), - rule, - ruleName, - expectedError, - }), + "ternary with wrong getter in both branches": [ + lintTest({ + code: createDispatcherCode( + nestedTernaryCode(wrongPropCode, wrongPropCode), + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ✅ Nested ternary with correct getter - "nested ternary with correct getter": lintTest({ - code: createDispatcherCode( - `\ + "nested ternary with correct getter": [ + lintTest({ + code: createDispatcherCode( + `\ return condition1 ? (condition2 - ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" })) - : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" });`, - ), - rule, - ruleName, - }), + ? new Person({ ${ID_PROP} ${propCode} name: "A" }) + : new Person({ ${ID_PROP} ${propCode} name: "B" })) + : new Person({ ${ID_PROP} ${propCode} name: "C" });`, + ), + rule, + ruleName, + }), + true, + ], // ✅ If/else with correct getter in both branches - "if else with correct getter in both branches": lintTest({ - code: createDispatcherCode( - `\ + "if else with correct getter in both branches": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition) { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); + return new Person({ ${ID_PROP} ${propCode} name: "A" }); } else { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); + return new Person({ ${ID_PROP} ${propCode} name: "B" }); }`, - ), - rule, - ruleName, - }), + ), + rule, + ruleName, + }), + true, + ], // ❌ If/else with wrong getter in if block - "if else with wrong getter in if block": lintTest({ - code: createDispatcherCode( - `\ + "if else with wrong getter in if block": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition) { - return new Person({ id: ctx.getActorUri(identifier), \ - ${wrongPropCode} name: "A" }); + return new Person({ ${ID_PROP} ${wrongPropCode} name: "A" }); } else { - return new Person({ id: ctx.getActorUri(identifier), \ -${propCode} name: "B" }); + return new Person({ ${ID_PROP} ${propCode} name: "B" }); }`, - ), - rule, - ruleName, - expectedError, - }), + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ❌ If/else with wrong getter in else block - "if else with wrong getter in else block": lintTest({ - code: createDispatcherCode( - `\ + "if else with wrong getter in else block": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition) { - return new Person({ id: ctx.getActorUri(identifier), \ -${propCode} name: "A" }); + return new Person({ ${ID_PROP} ${propCode} name: "A" }); } else { - return new Person({ id: ctx.getActorUri(identifier), \ -${wrongPropCode} name: "B" }); + return new Person({ ${ID_PROP} ${wrongPropCode} name: "B" }); }`, - ), - rule, - ruleName, - expectedError, - }), + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ❌ If/else with wrong getter in both blocks - "if else with wrong getter in both blocks": lintTest({ - code: createDispatcherCode( - `\ + "if else with wrong getter in both blocks": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition) { - return new Person({ id: ctx.getActorUri(identifier), \ -${wrongPropCode} name: "A" }); + return new Person({ ${ID_PROP} ${wrongPropCode} name: "A" }); } else { - return new Person({ id: ctx.getActorUri(identifier), \ -${wrongPropCode} name: "B" }); + return new Person({ ${ID_PROP} ${wrongPropCode} name: "B" }); }`, - ), - rule, - ruleName, - expectedError, - }), + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ✅ Nested if with correct getter - "nested if with correct getter": lintTest({ - code: createDispatcherCode( - `\ + "nested if with correct getter": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition1) { if (condition2) { - return new Person({ id: ctx.getActorUri(identifier), \ - ${propCode} name: "A" }); + return new Person({ ${ID_PROP} ${propCode} name: "A" }); } else { - return new Person({ id: ctx.getActorUri(identifier), \ - ${propCode} name: "B" }); + return new Person({ ${ID_PROP} ${propCode} name: "B" }); } } else { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" }); + return new Person({ ${ID_PROP} ${propCode} name: "C" }); }`, - ), - rule, - ruleName, - }), + ), + rule, + ruleName, + }), + true, + ], // ✅ If/else if/else with correct getter in all branches - "if else if else with correct getter in all branches": lintTest({ - code: createDispatcherCode( - `\ + "if else if else with correct getter in all branches": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition1) { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); + return new Person({ ${ID_PROP} ${propCode} name: "A" }); } else if (condition2) { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); + return new Person({ ${ID_PROP} ${propCode} name: "B" }); } else { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" }); + return new Person({ ${ID_PROP} ${propCode} name: "C" }); }`, - ), - rule, - ruleName, - }), + ), + rule, + ruleName, + }), + true, + ], // ❌ If/else if/else with wrong getter in else if - "if else if else with wrong getter in else if": lintTest({ - code: createDispatcherCode( - `\ + "if else if else with wrong getter in else if": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition1) { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); + return new Person({ ${ID_PROP} ${propCode} name: "A" }); } else if (condition2) { - return new Person({ id: ctx.getActorUri(identifier), ${wrongPropCode} name: "B" }); + return new Person({ ${ID_PROP} ${wrongPropCode} name: "B" }); } else { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" }); + return new Person({ ${ID_PROP} ${propCode} name: "C" }); }`, - ), - rule, - ruleName, - expectedError, - }), + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ✅ If/else if with final return, correct getter in all paths - "if else if with final return correct getter in all paths": lintTest({ - code: createDispatcherCode( - `\ + "if else if with final return correct getter in all paths": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition1) { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); + return new Person({ ${ID_PROP} ${propCode} name: "A" }); } else if (condition2) { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); + return new Person({ ${ID_PROP} ${propCode} name: "B" }); } -return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" });`, - ), - rule, - ruleName, - }), +return new Person({ ${ID_PROP} ${propCode} name: "C" });`, + ), + rule, + ruleName, + }), + true, + ], // ❌ If/else if with final return, wrong getter in final return - "if else if with final return wrong getter in final return": lintTest({ - code: createDispatcherCode( - `\ + "if else if with final return wrong getter in final return": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition1) { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); + return new Person({ ${ID_PROP} ${propCode} name: "A" }); } else if (condition2) { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); + return new Person({ ${ID_PROP} ${propCode} name: "B" }); } -return new Person({ id: ctx.getActorUri(identifier), ${wrongPropCode} name: "C" });`, - ), - rule, - ruleName, - expectedError, - }), +return new Person({ ${ID_PROP} ${wrongPropCode} name: "C" });`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], }; } const nestedTernaryCode = (prop1: string, prop2: string) => `return condition - ? new Person({ id: ctx.getActorUri(identifier), ${prop1} name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), ${prop2} name: "B" });`; + ? new Person({ ${ID_PROP} ${prop1} name: "A" }) + : new Person({ ${ID_PROP} ${prop2} name: "B" });`; /** * Creates edge case tests for id required rule */ -export function createIdRequiredEdgeCaseTests(config: TestConfig) { +export function createIdRequiredEdgeCaseTests(config: TestConfig): TestSuite { const { rule, ruleName } = config; const expectedError = actorPropertyRequired(properties.id); return { // ✅ Ternary with id in both branches - "ternary with id in both branches": lintTest({ - code: createDispatcherCode( - `return condition - ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), name: "B" });`, - ), - rule, - ruleName, - }), + "ternary with id in both branches": [ + lintTest({ + code: createDispatcherCode( + `return condition + ? new Person({ ${ID_PROP} name: "A" }) + : new Person({ ${ID_PROP} name: "B" });`, + ), + rule, + ruleName, + }), + true, + ], // ❌ Ternary missing id in consequent - "ternary missing id in consequent": lintTest({ - code: createDispatcherCode( - `return condition + "ternary missing id in consequent": [ + lintTest({ + code: createDispatcherCode( + `return condition ? new Person({ name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), name: "B" });`, - ), - rule, - ruleName, - expectedError, - }), + : new Person({ ${ID_PROP} name: "B" });`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ❌ Ternary missing id in alternate - "ternary missing id in alternate": lintTest({ - code: createDispatcherCode( - `return condition - ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) + "ternary missing id in alternate": [ + lintTest({ + code: createDispatcherCode( + `return condition + ? new Person({ ${ID_PROP} name: "A" }) : new Person({ name: "B" });`, - ), - rule, - ruleName, - expectedError, - }), + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ❌ Ternary missing id in both - "ternary missing id in both branches": lintTest({ - code: createDispatcherCode( - `return condition + "ternary missing id in both branches": [ + lintTest({ + code: createDispatcherCode( + `return condition ? new Person({ name: "A" }) : new Person({ name: "B" });`, - ), - rule, - ruleName, - expectedError, - }), + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ✅ Nested ternary with id - "nested ternary with id": lintTest({ - code: createDispatcherCode( - `return condition1 + "nested ternary with id": [ + lintTest({ + code: createDispatcherCode( + `return condition1 ? (condition2 - ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), name: "B" })) - : new Person({ id: ctx.getActorUri(identifier), name: "C" });`, - ), - rule, - ruleName, - }), + ? new Person({ ${ID_PROP} name: "A" }) + : new Person({ ${ID_PROP} name: "B" })) + : new Person({ ${ID_PROP} name: "C" });`, + ), + rule, + ruleName, + }), + true, + ], // ✅ If/else with id in both branches - "if else with id in both branches": lintTest({ - code: createDispatcherCode( - `\ + "if else with id in both branches": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition) { - return new Person({ id: ctx.getActorUri(identifier), name: "A" }); + return new Person({ ${ID_PROP} name: "A" }); } else { - return new Person({ id: ctx.getActorUri(identifier), name: "B" }); + return new Person({ ${ID_PROP} name: "B" }); }`, - ), - rule, - ruleName, - }), + ), + rule, + ruleName, + }), + true, + ], // ❌ If/else missing id in if block - "if else missing id in if block": lintTest({ - code: createDispatcherCode( - `\ + "if else missing id in if block": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition) { return new Person({ name: "A" }); } else { - return new Person({ id: ctx.getActorUri(identifier), name: "B" }); + return new Person({ ${ID_PROP} name: "B" }); }`, - ), - rule, - ruleName, - expectedError, - }), + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ❌ If/else missing id in else block - "if else missing id in else block": lintTest({ - code: createDispatcherCode( - `\ + "if else missing id in else block": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition) { - return new Person({ id: ctx.getActorUri(identifier), name: "A" }); + return new Person({ ${ID_PROP} name: "A" }); } else { return new Person({ name: "B" }); }`, - ), - rule, - ruleName, - expectedError, - }), + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ❌ If/else missing id in both blocks - "if else missing id in both blocks": lintTest({ - code: createDispatcherCode( - `\ + "if else missing id in both blocks": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition) { return new Person({ name: "A" }); } else { return new Person({ name: "B" }); }`, - ), - rule, - ruleName, - expectedError, - }), + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ✅ Nested if with id - "nested if with id": lintTest({ - code: createDispatcherCode( - `\ + "nested if with id": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition1) { if (condition2) { - return new Person({ id: ctx.getActorUri(identifier), name: "A" }); + return new Person({ ${ID_PROP} name: "A" }); } else { - return new Person({ id: ctx.getActorUri(identifier), name: "B" }); + return new Person({ ${ID_PROP} name: "B" }); } } else { - return new Person({ id: ctx.getActorUri(identifier), name: "C" }); + return new Person({ ${ID_PROP} name: "C" }); }`, - ), - rule, - ruleName, - }), + ), + rule, + ruleName, + }), + true, + ], // ✅ If/else if/else with id in all branches - "if else if else with id in all branches": lintTest({ - code: createDispatcherCode( - `\ + "if else if else with id in all branches": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition1) { - return new Person({ id: ctx.getActorUri(identifier), name: "A" }); + return new Person({ ${ID_PROP} name: "A" }); } else if (condition2) { - return new Person({ id: ctx.getActorUri(identifier), name: "B" }); + return new Person({ ${ID_PROP} name: "B" }); } else { - return new Person({ id: ctx.getActorUri(identifier), name: "C" }); + return new Person({ ${ID_PROP} name: "C" }); }`, - ), - rule, - ruleName, - }), + ), + rule, + ruleName, + }), + true, + ], // ❌ If/else if/else missing id in else if - "if else if else missing id in else if": lintTest({ - code: createDispatcherCode( - `\ + "if else if else missing id in else if": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition1) { - return new Person({ id: ctx.getActorUri(identifier), name: "A" }); + return new Person({ ${ID_PROP} name: "A" }); } else if (condition2) { return new Person({ name: "B" }); } else { - return new Person({ id: ctx.getActorUri(identifier), name: "C" }); + return new Person({ ${ID_PROP} name: "C" }); }`, - ), - rule, - ruleName, - expectedError, - }), + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ✅ If/else if with final return, id in all paths - "if else if with final return id in all paths": lintTest({ - code: createDispatcherCode( - `\ + "if else if with final return id in all paths": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition1) { - return new Person({ id: ctx.getActorUri(identifier), name: "A" }); + return new Person({ ${ID_PROP} name: "A" }); } else if (condition2) { - return new Person({ id: ctx.getActorUri(identifier), name: "B" }); + return new Person({ ${ID_PROP} name: "B" }); } -return new Person({ id: ctx.getActorUri(identifier), name: "C" });`, - ), - rule, - ruleName, - }), +return new Person({ ${ID_PROP} name: "C" });`, + ), + rule, + ruleName, + }), + true, + ], // ❌ If/else if with final return, missing id in final return - "if else if with final return missing id in final return": lintTest({ - code: createDispatcherCode( - `\ + "if else if with final return missing id in final return": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition1) { - return new Person({ id: ctx.getActorUri(identifier), name: "A" }); + return new Person({ ${ID_PROP} name: "A" }); } else if (condition2) { - return new Person({ id: ctx.getActorUri(identifier), name: "B" }); + return new Person({ ${ID_PROP} name: "B" }); } return new Person({ name: "C" });`, - ), - rule, - ruleName, - expectedError, - }), + ), + rule, + ruleName, + expectedError, + }), + false, + ], }; } /** * Creates edge case tests for id mismatch rule */ -export function createIdMismatchEdgeCaseTests(config: TestConfig) { +export function createIdMismatchEdgeCaseTests(config: TestConfig): TestSuite { const { rule, ruleName } = config; const expectedError = actorPropertyMismatch( createMethodCallContext(properties.id), @@ -1468,215 +1421,257 @@ export function createIdMismatchEdgeCaseTests(config: TestConfig) { return { // ✅ Ternary with correct getter in both branches - "ternary with correct getter in both branches": lintTest({ - code: createDispatcherCode( - `return condition - ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), name: "B" });`, - ), - rule, - ruleName, - }), + "ternary with correct getter in both branches": [ + lintTest({ + code: createDispatcherCode( + `return condition + ? new Person({ ${ID_PROP} name: "A" }) + : new Person({ ${ID_PROP} name: "B" });`, + ), + rule, + ruleName, + }), + true, + ], // ❌ Ternary with wrong getter in consequent - "ternary with wrong getter in consequent": lintTest({ - code: createDispatcherCode( - `return condition + "ternary with wrong getter in consequent": [ + lintTest({ + code: createDispatcherCode( + `return condition ? new Person({ id: ctx.getFollowingUri(identifier), name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), name: "B" });`, - ), - rule, - ruleName, - expectedError, - }), + : new Person({ ${ID_PROP} name: "B" });`, + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ❌ Ternary with wrong getter in alternate - "ternary with wrong getter in alternate": lintTest({ - code: createDispatcherCode( - `return condition - ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) + "ternary with wrong getter in alternate": [ + lintTest({ + code: createDispatcherCode( + `return condition + ? new Person({ ${ID_PROP} name: "A" }) : new Person({ id: ctx.getFollowingUri(identifier), name: "B" });`, - ), - rule, - ruleName, - expectedError, - }), + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ❌ Ternary with wrong getter in both - "ternary with wrong getter in both branches": lintTest({ - code: createDispatcherCode( - `return condition + "ternary with wrong getter in both branches": [ + lintTest({ + code: createDispatcherCode( + `return condition ? new Person({ id: ctx.getFollowingUri(identifier), name: "A" }) : new Person({ id: ctx.getFollowingUri(identifier), name: "B" });`, - ), - rule, - ruleName, - expectedError, - }), + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ✅ Nested ternary with correct getter - "nested ternary with correct getter": lintTest({ - code: createDispatcherCode( - `return condition1 + "nested ternary with correct getter": [ + lintTest({ + code: createDispatcherCode( + `return condition1 ? (condition2 - ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), name: "B" })) - : new Person({ id: ctx.getActorUri(identifier), name: "C" });`, - ), - rule, - ruleName, - }), + ? new Person({ ${ID_PROP} name: "A" }) + : new Person({ ${ID_PROP} name: "B" })) + : new Person({ ${ID_PROP} name: "C" });`, + ), + rule, + ruleName, + }), + true, + ], // ✅ If/else with correct getter in both branches - "if else with correct getter in both branches": lintTest({ - code: createDispatcherCode( - `\ + "if else with correct getter in both branches": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition) { - return new Person({ id: ctx.getActorUri(identifier), name: "A" }); + return new Person({ ${ID_PROP} name: "A" }); } else { - return new Person({ id: ctx.getActorUri(identifier), name: "B" }); + return new Person({ ${ID_PROP} name: "B" }); }`, - ), - rule, - ruleName, - }), + ), + rule, + ruleName, + }), + true, + ], // ❌ If/else with wrong getter in if block - "if else with wrong getter in if block": lintTest({ - code: createDispatcherCode( - `\ + "if else with wrong getter in if block": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition) { return new Person({ id: ctx.getFollowingUri(identifier), name: "A" }); } else { - return new Person({ id: ctx.getActorUri(identifier), name: "B" }); + return new Person({ ${ID_PROP} name: "B" }); }`, - ), - rule, - ruleName, - expectedError, - }), + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ❌ If/else with wrong getter in else block - "if else with wrong getter in else block": lintTest({ - code: createDispatcherCode( - `\ + "if else with wrong getter in else block": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition) { - return new Person({ id: ctx.getActorUri(identifier), name: "A" }); + return new Person({ ${ID_PROP} name: "A" }); } else { return new Person({ id: ctx.getFollowingUri(identifier), name: "B" }); }`, - ), - rule, - ruleName, - expectedError, - }), + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ❌ If/else with wrong getter in both blocks - "if else with wrong getter in both blocks": lintTest({ - code: createDispatcherCode( - `\ + "if else with wrong getter in both blocks": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition) { return new Person({ id: ctx.getFollowingUri(identifier), name: "A" }); } else { return new Person({ id: ctx.getFollowingUri(identifier), name: "B" }); }`, - ), - rule, - ruleName, - expectedError, - }), + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ✅ Nested if with correct getter - "nested if with correct getter": lintTest({ - code: createDispatcherCode( - `\ + "nested if with correct getter": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition1) { if (condition2) { - return new Person({ id: ctx.getActorUri(identifier), name: "A" }); + return new Person({ ${ID_PROP} name: "A" }); } else { - return new Person({ id: ctx.getActorUri(identifier), name: "B" }); + return new Person({ ${ID_PROP} name: "B" }); } } else { - return new Person({ id: ctx.getActorUri(identifier), name: "C" }); + return new Person({ ${ID_PROP} name: "C" }); }`, - ), - rule, - ruleName, - }), + ), + rule, + ruleName, + }), + true, + ], // ✅ If/else if/else with correct getter in all branches - "if else if else with correct getter in all branches": lintTest({ - code: createDispatcherCode( - `\ + "if else if else with correct getter in all branches": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition1) { - return new Person({ id: ctx.getActorUri(identifier), name: "A" }); + return new Person({ ${ID_PROP} name: "A" }); } else if (condition2) { - return new Person({ id: ctx.getActorUri(identifier), name: "B" }); + return new Person({ ${ID_PROP} name: "B" }); } else { - return new Person({ id: ctx.getActorUri(identifier), name: "C" }); + return new Person({ ${ID_PROP} name: "C" }); }`, - ), - rule, - ruleName, - }), + ), + rule, + ruleName, + }), + true, + ], // ❌ If/else if/else with wrong getter in else if - "if else if else with wrong getter in else if": lintTest({ - code: createDispatcherCode( - `\ + "if else if else with wrong getter in else if": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition1) { - return new Person({ id: ctx.getActorUri(identifier), name: "A" }); + return new Person({ ${ID_PROP} name: "A" }); } else if (condition2) { return new Person({ id: ctx.getFollowingUri(identifier), name: "B" }); } else { - return new Person({ id: ctx.getActorUri(identifier), name: "C" }); + return new Person({ ${ID_PROP} name: "C" }); }`, - ), - rule, - ruleName, - expectedError, - }), + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ✅ If/else if with final return, correct getter in all paths - "if else if with final return correct getter in all paths": lintTest({ - code: createDispatcherCode( - `\ + "if else if with final return correct getter in all paths": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition1) { - return new Person({ id: ctx.getActorUri(identifier), name: "A" }); + return new Person({ ${ID_PROP} name: "A" }); } else if (condition2) { - return new Person({ id: ctx.getActorUri(identifier), name: "B" }); + return new Person({ ${ID_PROP} name: "B" }); } -return new Person({ id: ctx.getActorUri(identifier), name: "C" });`, - ), - rule, - ruleName, - }), +return new Person({ ${ID_PROP} name: "C" });`, + ), + rule, + ruleName, + }), + true, + ], // ❌ If/else if with final return, wrong getter in final return - "if else if with final return wrong getter in final return": lintTest({ - code: createDispatcherCode( - `\ + "if else if with final return wrong getter in final return": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition1) { - return new Person({ id: ctx.getActorUri(identifier), name: "A" }); + return new Person({ ${ID_PROP} name: "A" }); } else if (condition2) { - return new Person({ id: ctx.getActorUri(identifier), name: "B" }); + return new Person({ ${ID_PROP} name: "B" }); } return new Person({ id: ctx.getFollowingUri(identifier), name: "C" });`, - ), - rule, - ruleName, - expectedError, - }), + ), + rule, + ruleName, + expectedError, + }), + false, + ], }; } /** * Creates edge case tests for key required rules (publicKey, assertionMethod) */ -export function createKeyRequiredEdgeCaseTests( +export function _createRequiredEdgeCaseTests( propertyName: "publicKey" | "assertionMethod", config: TestConfig, -) { +): TestSuite { const { rule, ruleName } = config; const prop = properties[propertyName]; const expectedError = actorPropertyRequired(prop); @@ -1687,219 +1682,261 @@ export function createKeyRequiredEdgeCaseTests( return { // ✅ Ternary with property in both branches - "ternary with property in both branches": lintTest({ - code: createChainedDispatcherCode( - nestedTernaryCode(propCode, propCode), - "setKeyPairsDispatcher", - ), - rule, - ruleName, - }), + "ternary with property in both branches": [ + lintTest({ + code: createChainedDispatcherCode( + nestedTernaryCode(propCode, propCode), + "setKeyPairsDispatcher", + ), + rule, + ruleName, + }), + true, + ], // ❌ Ternary missing property in consequent - "ternary missing property in consequent": lintTest({ - code: createChainedDispatcherCode( - `\ + "ternary missing property in consequent": [ + lintTest({ + code: createChainedDispatcherCode( + `\ return condition - ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" });`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - expectedError, - }), + ? new Person({ ${ID_PROP} name: "A" }) + : new Person({ ${ID_PROP} ${propCode} name: "B" });`, + "setKeyPairsDispatcher", + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ❌ Ternary missing property in alternate - "ternary missing property in alternate": lintTest({ - code: createChainedDispatcherCode( - `\ + "ternary missing property in alternate": [ + lintTest({ + code: createChainedDispatcherCode( + `\ return condition - ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), name: "B" });`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - expectedError, - }), + ? new Person({ ${ID_PROP} ${propCode} name: "A" }) + : new Person({ ${ID_PROP} name: "B" });`, + "setKeyPairsDispatcher", + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ❌ Ternary missing property in both - "ternary missing property in both branches": lintTest({ - code: createChainedDispatcherCode( - `return condition - ? new Person({ id: ctx.getActorUri(identifier), name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), name: "B" });`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - expectedError, - }), + "ternary missing property in both branches": [ + lintTest({ + code: createChainedDispatcherCode( + `return condition + ? new Person({ ${ID_PROP} name: "A" }) + : new Person({ ${ID_PROP} name: "B" });`, + "setKeyPairsDispatcher", + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ✅ Nested ternary with property - "nested ternary with property": lintTest({ - code: createChainedDispatcherCode( - `\ + "nested ternary with property": [ + lintTest({ + code: createChainedDispatcherCode( + `\ return condition1 ? (condition2 - ? new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }) - : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" })) - : new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" });`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - }), + ? new Person({ ${ID_PROP} ${propCode} name: "A" }) + : new Person({ ${ID_PROP} ${propCode} name: "B" })) + : new Person({ ${ID_PROP} ${propCode} name: "C" });`, + "setKeyPairsDispatcher", + ), + rule, + ruleName, + }), + true, + ], // ✅ If/else with property in both branches - "if else with property in both branches": lintTest({ - code: createChainedDispatcherCode( - `\ + "if else with property in both branches": [ + lintTest({ + code: createChainedDispatcherCode( + `\ if (condition) { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); + return new Person({ ${ID_PROP} ${propCode} name: "A" }); } else { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); + return new Person({ ${ID_PROP} ${propCode} name: "B" }); }`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - }), + "setKeyPairsDispatcher", + ), + rule, + ruleName, + }), + true, + ], // ❌ If/else missing property in if block - "if else missing property in if block": lintTest({ - code: createChainedDispatcherCode( - `\ + "if else missing property in if block": [ + lintTest({ + code: createChainedDispatcherCode( + `\ if (condition) { - return new Person({ id: ctx.getActorUri(identifier), name: "A" }); + return new Person({ ${ID_PROP} name: "A" }); } else { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); + return new Person({ ${ID_PROP} ${propCode} name: "B" }); }`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - expectedError, - }), + "setKeyPairsDispatcher", + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ❌ If/else missing property in else block - "if else missing property in else block": lintTest({ - code: createChainedDispatcherCode( - `\ + "if else missing property in else block": [ + lintTest({ + code: createChainedDispatcherCode( + `\ if (condition) { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); + return new Person({ ${ID_PROP} ${propCode} name: "A" }); } else { - return new Person({ id: ctx.getActorUri(identifier), name: "B" }); + return new Person({ ${ID_PROP} name: "B" }); }`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - expectedError, - }), + "setKeyPairsDispatcher", + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ❌ If/else missing property in both blocks - "if else missing property in both blocks": lintTest({ - code: createChainedDispatcherCode( - `\ + "if else missing property in both blocks": [ + lintTest({ + code: createChainedDispatcherCode( + `\ if (condition) { - return new Person({ id: ctx.getActorUri(identifier), name: "A" }); + return new Person({ ${ID_PROP} name: "A" }); } else { - return new Person({ id: ctx.getActorUri(identifier), name: "B" }); + return new Person({ ${ID_PROP} name: "B" }); }`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - expectedError, - }), + "setKeyPairsDispatcher", + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ✅ Nested if with property - "nested if with property": lintTest({ - code: createChainedDispatcherCode( - `\ + "nested if with property": [ + lintTest({ + code: createChainedDispatcherCode( + `\ if (condition1) { if (condition2) { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); + return new Person({ ${ID_PROP} ${propCode} name: "A" }); } else { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); + return new Person({ ${ID_PROP} ${propCode} name: "B" }); } } else { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" }); + return new Person({ ${ID_PROP} ${propCode} name: "C" }); }`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - }), + "setKeyPairsDispatcher", + ), + rule, + ruleName, + }), + true, + ], // ✅ If/else if/else with property in all branches - "if else if else with property in all branches": lintTest({ - code: createChainedDispatcherCode( - `\ + "if else if else with property in all branches": [ + lintTest({ + code: createChainedDispatcherCode( + `\ if (condition1) { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); + return new Person({ ${ID_PROP} ${propCode} name: "A" }); } else if (condition2) { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); + return new Person({ ${ID_PROP} ${propCode} name: "B" }); } else { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" }); + return new Person({ ${ID_PROP} ${propCode} name: "C" }); }`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - }), + "setKeyPairsDispatcher", + ), + rule, + ruleName, + }), + true, + ], // ❌ If/else if/else missing property in else if - "if else if else missing property in else if": lintTest({ - code: createChainedDispatcherCode( - `\ + "if else if else missing property in else if": [ + lintTest({ + code: createChainedDispatcherCode( + `\ if (condition1) { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); + return new Person({ ${ID_PROP} ${propCode} name: "A" }); } else if (condition2) { - return new Person({ id: ctx.getActorUri(identifier), name: "B" }); + return new Person({ ${ID_PROP} name: "B" }); } else { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" }); + return new Person({ ${ID_PROP} ${propCode} name: "C" }); }`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - expectedError, - }), + "setKeyPairsDispatcher", + ), + rule, + ruleName, + expectedError, + }), + false, + ], // ✅ If/else if with final return, property in all paths - "if else if with final return property in all paths": lintTest({ - code: createChainedDispatcherCode( - `\ + "if else if with final return property in all paths": [ + lintTest({ + code: createChainedDispatcherCode( + `\ if (condition1) { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); + return new Person({ ${ID_PROP} ${propCode} name: "A" }); } else if (condition2) { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); + return new Person({ ${ID_PROP} ${propCode} name: "B" }); } -return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "C" });`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - }), +return new Person({ ${ID_PROP} ${propCode} name: "C" });`, + "setKeyPairsDispatcher", + ), + rule, + ruleName, + }), + true, + ], // ❌ If/else if with final return, missing property in final return - "if else if with final return missing property in final return": lintTest({ - code: createChainedDispatcherCode( - `\ + "if else if with final return missing property in final return": [ + lintTest({ + code: createChainedDispatcherCode( + `\ if (condition1) { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "A" }); + return new Person({ ${ID_PROP} ${propCode} name: "A" }); } else if (condition2) { - return new Person({ id: ctx.getActorUri(identifier), ${propCode} name: "B" }); + return new Person({ ${ID_PROP} ${propCode} name: "B" }); } -return new Person({ id: ctx.getActorUri(identifier), name: "C" });`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - expectedError, - }), +return new Person({ ${ID_PROP} name: "C" });`, + "setKeyPairsDispatcher", + ), + rule, + ruleName, + expectedError, + }), + false, + ], }; } diff --git a/packages/lint/src/tests/actor-assertion-method-required.test.ts b/packages/lint/src/tests/actor-assertion-method-required.test.ts index ba8b01855..d0bb75e44 100644 --- a/packages/lint/src/tests/actor-assertion-method-required.test.ts +++ b/packages/lint/src/tests/actor-assertion-method-required.test.ts @@ -1,264 +1,16 @@ -import { test } from "node:test"; -import { properties, RULE_IDS } from "../lib/const.ts"; -import { actorPropertyRequired } from "../lib/messages.ts"; -import { createKeyRequiredEdgeCaseTests } from "../lib/test-templates.ts"; -import lintTest from "../lib/test.ts"; +import { RULE_IDS } from "../lib/const.ts"; +import { + createRequiredDispatcherRuleTests, + createRequiredEdgeCaseTests, + runTests, +} from "../lib/test-templates.ts"; import * as rule from "../rules/actor-assertion-method-required.ts"; const ruleName = RULE_IDS.actorAssertionMethodRequired; - -test( - `${ruleName}: ✅ Good - \`setActorDispatcher\` called on non-Federation object`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ - name: "John Doe", - }); - }); - `, - rule, - ruleName, - federationSetup: ` - const federation = { - setActorDispatcher: () => {} - }; - `, - }), -); - -test( - `${ruleName}: ✅ Good - key pairs dispatcher NOT configured, property missing (no error)`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ - id: ctx.getActorUri(identifier), - name: "John Doe", - }); - }); - `, - rule, - ruleName, - }), -); - -test( - `${ruleName}: ✅ Good - key pairs dispatcher configured BEFORE setActorDispatcher, property present`, - lintTest({ - code: ` - federation.setKeyPairsDispatcher(async (ctx, identifier) => { - return []; - }); - - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ - id: ctx.getActorUri(identifier), - assertionMethod: ctx.getActorKeyPairs(identifier), - name: "John Doe", - }); - }); - `, - rule, - ruleName, - }), -); - -test( - `${ruleName}: ✅ Good - key pairs dispatcher configured BEFORE setActorDispatcher (chained), property present`, - lintTest({ - code: ` - federation - .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ - id: ctx.getActorUri(identifier), - assertionMethod: ctx.getActorKeyPairs(identifier), - name: "John Doe", - }); - }) - .setKeyPairsDispatcher(async (ctx, identifier) => []); - `, - rule, - ruleName, - }), -); - -test( - `${ruleName}: ✅ Good - key pairs dispatcher configured AFTER setActorDispatcher, property present`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ - id: ctx.getActorUri(identifier), - assertionMethod: ctx.getActorKeyPairs(identifier), - name: "John Doe", - }); - }); - - federation.setKeyPairsDispatcher(async (ctx, identifier) => { - return []; - }); - `, - rule, - ruleName, - }), -); - -test( - `${ruleName}: ✅ Good - key pairs dispatcher configured AFTER setActorDispatcher (chained), property present`, - lintTest({ - code: ` - federation - .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ - id: ctx.getActorUri(identifier), - assertionMethod: ctx.getActorKeyPairs(identifier), - name: "John Doe", - }); - }) - .setKeyPairsDispatcher(async (ctx, identifier) => []); - `, - rule, - ruleName, - }), -); - -test( - `${ruleName}: ❌ Bad - key pairs dispatcher configured BEFORE, property missing`, - lintTest({ - code: ` - federation.setKeyPairsDispatcher(async (ctx, identifier) => { - return []; - }); - - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ - id: ctx.getActorUri(identifier), - name: "John Doe", - }); - }); - `, - rule, - ruleName, - expectedError: actorPropertyRequired(properties.assertionMethod), - }), -); - -test( - `${ruleName}: ❌ Bad - key pairs dispatcher configured BEFORE (chained), property missing`, - lintTest({ - code: ` - federation - .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ - id: ctx.getActorUri(identifier), - name: "John Doe", - }); - }) - .setKeyPairsDispatcher(async (ctx, identifier) => []); - `, - rule, - ruleName, - expectedError: actorPropertyRequired(properties.assertionMethod), - }), -); - -test( - `${ruleName}: ❌ Bad - key pairs dispatcher configured AFTER, property missing`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ - id: ctx.getActorUri(identifier), - name: "John Doe", - }); - }); - - federation.setKeyPairsDispatcher(async (ctx, identifier) => { - return []; - }); - `, - rule, - ruleName, - expectedError: actorPropertyRequired(properties.assertionMethod), - }), -); - -test( - `${ruleName}: ❌ Bad - key pairs dispatcher configured AFTER (chained), property missing`, - lintTest({ - code: ` - federation - .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ - id: ctx.getActorUri(identifier), - name: "John Doe", - }); - }) - .setKeyPairsDispatcher(async (ctx, identifier) => []); - `, - rule, - ruleName, - expectedError: actorPropertyRequired(properties.assertionMethod), - }), -); - -// Edge case tests const config = { rule, ruleName }; -const edgeCases = createKeyRequiredEdgeCaseTests("assertionMethod", config); -test( - `${ruleName}: ✅ Edge - ternary with property in both branches`, - edgeCases["ternary with property in both branches"], -); -test( - `${ruleName}: ❌ Edge - ternary missing property in consequent`, - edgeCases["ternary missing property in consequent"], -); -test( - `${ruleName}: ❌ Edge - ternary missing property in alternate`, - edgeCases["ternary missing property in alternate"], -); -test( - `${ruleName}: ❌ Edge - ternary missing property in both branches`, - edgeCases["ternary missing property in both branches"], -); -test( - `${ruleName}: ✅ Edge - nested ternary with property`, - edgeCases["nested ternary with property"], -); -test( - `${ruleName}: ✅ Edge - if/else with property in both branches`, - edgeCases["if else with property in both branches"], -); -test( - `${ruleName}: ❌ Edge - if/else missing property in if block`, - edgeCases["if else missing property in if block"], -); -test( - `${ruleName}: ❌ Edge - if/else missing property in else block`, - edgeCases["if else missing property in else block"], -); -test( - `${ruleName}: ❌ Edge - if/else missing property in both blocks`, - edgeCases["if else missing property in both blocks"], -); -test( - `${ruleName}: ✅ Edge - nested if with property`, - edgeCases["nested if with property"], -); -test( - `${ruleName}: ✅ Edge - if else if else with property in all branches`, - edgeCases["if else if else with property in all branches"], -); -test( - `${ruleName}: ❌ Edge - if else if else missing property in else if`, - edgeCases["if else if else missing property in else if"], -); -test( - `${ruleName}: ✅ Edge - if else if with final return property in all paths`, - edgeCases["if else if with final return property in all paths"], -); -test( - `${ruleName}: ❌ Edge - if else if with final return missing property in final return`, - edgeCases["if else if with final return missing property in final return"], + +runTests( + ruleName, + createRequiredDispatcherRuleTests("assertionMethod", config), ); +runTests(ruleName, createRequiredEdgeCaseTests("assertionMethod", config)); diff --git a/packages/lint/src/tests/actor-featured-property-mismatch.test.ts b/packages/lint/src/tests/actor-featured-property-mismatch.test.ts index 02c659a00..bca8148bb 100644 --- a/packages/lint/src/tests/actor-featured-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-featured-property-mismatch.test.ts @@ -1,87 +1,13 @@ -import { test } from "node:test"; import { RULE_IDS } from "../lib/const.ts"; import { createMismatchEdgeCaseTests, createMismatchRuleTests, + runTests, } from "../lib/test-templates.ts"; import * as rule from "../rules/actor-featured-property-mismatch.ts"; const ruleName = RULE_IDS.actorFeaturedPropertyMismatch; - const config = { rule, ruleName }; -// Standard mismatch rule tests -const tests = createMismatchRuleTests("featured", config); -test( - `${ruleName}: ✅ Good - non-Federation object`, - tests["non-federation object"], -); -test( - `${ruleName}: ✅ Good - correct getter used`, - tests["correct getter used"], -); -test( - `${ruleName}: ✅ Good - property not present`, - tests["property not present"], -); -test(`${ruleName}: ❌ Bad - wrong getter used`, tests["wrong getter used"]); -test(`${ruleName}: ❌ Bad - wrong identifier`, tests["wrong identifier"]); - -// Edge case tests -const edgeCases = createMismatchEdgeCaseTests("featured", config); -test( - `${ruleName}: ✅ Edge - ternary with correct getter in both branches`, - edgeCases["ternary with correct getter in both branches"], -); -test( - `${ruleName}: ❌ Edge - ternary with wrong getter in consequent`, - edgeCases["ternary with wrong getter in consequent"], -); -test( - `${ruleName}: ❌ Edge - ternary with wrong getter in alternate`, - edgeCases["ternary with wrong getter in alternate"], -); -test( - `${ruleName}: ❌ Edge - ternary with wrong getter in both branches`, - edgeCases["ternary with wrong getter in both branches"], -); -test( - `${ruleName}: ✅ Edge - nested ternary with correct getter`, - edgeCases["nested ternary with correct getter"], -); -test( - `${ruleName}: ✅ Edge - if/else with correct getter in both branches`, - edgeCases["if else with correct getter in both branches"], -); -test( - `${ruleName}: ❌ Edge - if/else with wrong getter in if block`, - edgeCases["if else with wrong getter in if block"], -); -test( - `${ruleName}: ❌ Edge - if/else with wrong getter in else block`, - edgeCases["if else with wrong getter in else block"], -); -test( - `${ruleName}: ❌ Edge - if/else with wrong getter in both blocks`, - edgeCases["if else with wrong getter in both blocks"], -); -test( - `${ruleName}: ✅ Edge - nested if with correct getter`, - edgeCases["nested if with correct getter"], -); -test( - `${ruleName}: ✅ Edge - if else if else with correct getter in all branches`, - edgeCases["if else if else with correct getter in all branches"], -); -test( - `${ruleName}: ❌ Edge - if else if else with wrong getter in else if`, - edgeCases["if else if else with wrong getter in else if"], -); -test( - `${ruleName}: ✅ Edge - if else if with final return correct getter in all paths`, - edgeCases["if else if with final return correct getter in all paths"], -); -test( - `${ruleName}: ❌ Edge - if else if with final return wrong getter in final return`, - edgeCases["if else if with final return wrong getter in final return"], -); +runTests(ruleName, createMismatchRuleTests("featured", config)); +runTests(ruleName, createMismatchEdgeCaseTests("featured", config)); diff --git a/packages/lint/src/tests/actor-featured-property-required.test.ts b/packages/lint/src/tests/actor-featured-property-required.test.ts index 22e79310a..9e72bd401 100644 --- a/packages/lint/src/tests/actor-featured-property-required.test.ts +++ b/packages/lint/src/tests/actor-featured-property-required.test.ts @@ -1,117 +1,16 @@ -import { test } from "node:test"; import { RULE_IDS } from "../lib/const.ts"; import { createRequiredDispatcherRuleTests, createRequiredEdgeCaseTests, + runTests, } from "../lib/test-templates.ts"; import * as rule from "../rules/actor-featured-property-required.ts"; const ruleName = RULE_IDS.actorFeaturedPropertyRequired; - const config = { rule, ruleName }; -// Standard required dispatcher rule tests -const tests = createRequiredDispatcherRuleTests("featured", config); -test( - `${ruleName}: ✅ Good - non-Federation object`, - tests["non-federation object"], -); -test( - `${ruleName}: ✅ Good - dispatcher NOT configured`, - tests["dispatcher not configured"], -); -test( - `${ruleName}: ✅ Good - dispatcher BEFORE (chained) with property`, - tests["dispatcher before chained with property"], -); -test( - `${ruleName}: ✅ Good - dispatcher BEFORE (separate) with property`, - tests["dispatcher before separate with property"], -); -test( - `${ruleName}: ✅ Good - dispatcher AFTER (chained) with property`, - tests["dispatcher after chained with property"], -); -test( - `${ruleName}: ✅ Good - dispatcher AFTER (separate) with property`, - tests["dispatcher after separate with property"], -); -test( - `${ruleName}: ❌ Bad - dispatcher configured, property missing`, - tests["dispatcher configured property missing"], -); -test( - `${ruleName}: ❌ Bad - dispatcher BEFORE (separate), property missing`, - tests["dispatcher before separate property missing"], -); -test( - `${ruleName}: ❌ Bad - dispatcher AFTER (separate), property missing`, - tests["dispatcher after separate property missing"], -); -test( - `${ruleName}: ❌ Bad - variable assignment without property`, - tests["variable assignment without property"], -); - -// Edge case tests -const edgeCases = createRequiredEdgeCaseTests( - "featured", - config, - "setFeaturedDispatcher", -); -test( - `${ruleName}: ✅ Edge - ternary with property in both branches`, - edgeCases["ternary with property in both branches"], -); -test( - `${ruleName}: ❌ Edge - ternary missing property in consequent`, - edgeCases["ternary missing property in consequent"], -); -test( - `${ruleName}: ❌ Edge - ternary missing property in alternate`, - edgeCases["ternary missing property in alternate"], -); -test( - `${ruleName}: ❌ Edge - ternary missing property in both branches`, - edgeCases["ternary missing property in both branches"], -); -test( - `${ruleName}: ✅ Edge - nested ternary with property`, - edgeCases["nested ternary with property"], -); -test( - `${ruleName}: ✅ Edge - if/else with property in both branches`, - edgeCases["if else with property in both branches"], -); -test( - `${ruleName}: ❌ Edge - if/else missing property in if block`, - edgeCases["if else missing property in if block"], -); -test( - `${ruleName}: ❌ Edge - if/else missing property in else block`, - edgeCases["if else missing property in else block"], -); -test( - `${ruleName}: ❌ Edge - if/else missing property in both blocks`, - edgeCases["if else missing property in both blocks"], -); -test( - `${ruleName}: ✅ Edge - nested if with property`, - edgeCases["nested if with property"], -); -test( - `${ruleName}: ✅ Edge - if else if else with featured in all branches`, - edgeCases["if else if else with property in all branches"], -); -test( - `${ruleName}: ❌ Edge - if else if else missing featured in else if`, - edgeCases["if else if else missing property in else if"], -); -test( - `${ruleName}: ✅ Edge - if else if with final return featured in all paths`, - edgeCases["if else if with final return property in all paths"], -); -test( - `${ruleName}: ❌ Edge - if else if with final return missing featured in final return`, - edgeCases["if else if with final return missing property in final return"], +runTests(ruleName, createRequiredDispatcherRuleTests("featured", config)); +runTests( + ruleName, + createRequiredEdgeCaseTests("featured", config), ); diff --git a/packages/lint/src/tests/actor-featured-tags-property-mismatch.test.ts b/packages/lint/src/tests/actor-featured-tags-property-mismatch.test.ts index 1322eead4..431bfcbe0 100644 --- a/packages/lint/src/tests/actor-featured-tags-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-featured-tags-property-mismatch.test.ts @@ -1,87 +1,13 @@ -import { test } from "node:test"; import { RULE_IDS } from "../lib/const.ts"; import { createMismatchEdgeCaseTests, createMismatchRuleTests, + runTests, } from "../lib/test-templates.ts"; import * as rule from "../rules/actor-featured-tags-property-mismatch.ts"; const ruleName = RULE_IDS.actorFeaturedTagsPropertyMismatch; - const config = { rule, ruleName }; -// Standard mismatch rule tests -const tests = createMismatchRuleTests("featuredTags", config); -test( - `${ruleName}: ✅ Good - non-Federation object`, - tests["non-federation object"], -); -test( - `${ruleName}: ✅ Good - correct getter used`, - tests["correct getter used"], -); -test( - `${ruleName}: ✅ Good - property not present`, - tests["property not present"], -); -test(`${ruleName}: ❌ Bad - wrong getter used`, tests["wrong getter used"]); -test(`${ruleName}: ❌ Bad - wrong identifier`, tests["wrong identifier"]); - -// Edge case tests -const edgeCases = createMismatchEdgeCaseTests("featuredTags", config); -test( - `${ruleName}: ✅ Edge - ternary with correct getter in both branches`, - edgeCases["ternary with correct getter in both branches"], -); -test( - `${ruleName}: ❌ Edge - ternary with wrong getter in consequent`, - edgeCases["ternary with wrong getter in consequent"], -); -test( - `${ruleName}: ❌ Edge - ternary with wrong getter in alternate`, - edgeCases["ternary with wrong getter in alternate"], -); -test( - `${ruleName}: ❌ Edge - ternary with wrong getter in both branches`, - edgeCases["ternary with wrong getter in both branches"], -); -test( - `${ruleName}: ✅ Edge - nested ternary with correct getter`, - edgeCases["nested ternary with correct getter"], -); -test( - `${ruleName}: ✅ Edge - if/else with correct getter in both branches`, - edgeCases["if else with correct getter in both branches"], -); -test( - `${ruleName}: ❌ Edge - if/else with wrong getter in if block`, - edgeCases["if else with wrong getter in if block"], -); -test( - `${ruleName}: ❌ Edge - if/else with wrong getter in else block`, - edgeCases["if else with wrong getter in else block"], -); -test( - `${ruleName}: ❌ Edge - if/else with wrong getter in both blocks`, - edgeCases["if else with wrong getter in both blocks"], -); -test( - `${ruleName}: ✅ Edge - nested if with correct getter`, - edgeCases["nested if with correct getter"], -); -test( - `${ruleName}: ✅ Edge - if else if else with correct getter in all branches`, - edgeCases["if else if else with correct getter in all branches"], -); -test( - `${ruleName}: ❌ Edge - if else if else with wrong getter in else if`, - edgeCases["if else if else with wrong getter in else if"], -); -test( - `${ruleName}: ✅ Edge - if else if with final return correct getter in all paths`, - edgeCases["if else if with final return correct getter in all paths"], -); -test( - `${ruleName}: ❌ Edge - if else if with final return wrong getter in final return`, - edgeCases["if else if with final return wrong getter in final return"], -); +runTests(ruleName, createMismatchRuleTests("featuredTags", config)); +runTests(ruleName, createMismatchEdgeCaseTests("featuredTags", config)); diff --git a/packages/lint/src/tests/actor-featured-tags-property-required.test.ts b/packages/lint/src/tests/actor-featured-tags-property-required.test.ts index 1caafc59d..f612689ed 100644 --- a/packages/lint/src/tests/actor-featured-tags-property-required.test.ts +++ b/packages/lint/src/tests/actor-featured-tags-property-required.test.ts @@ -1,117 +1,19 @@ -import { test } from "node:test"; import { RULE_IDS } from "../lib/const.ts"; import { createRequiredDispatcherRuleTests, createRequiredEdgeCaseTests, + runTests, } from "../lib/test-templates.ts"; import * as rule from "../rules/actor-featured-tags-property-required.ts"; const ruleName = RULE_IDS.actorFeaturedTagsPropertyRequired; - const config = { rule, ruleName }; -// Standard required dispatcher rule tests -const tests = createRequiredDispatcherRuleTests("featuredTags", config); -test( - `${ruleName}: ✅ Good - non-Federation object`, - tests["non-federation object"], -); -test( - `${ruleName}: ✅ Good - dispatcher NOT configured`, - tests["dispatcher not configured"], -); -test( - `${ruleName}: ✅ Good - dispatcher BEFORE (chained) with property`, - tests["dispatcher before chained with property"], -); -test( - `${ruleName}: ✅ Good - dispatcher BEFORE (separate) with property`, - tests["dispatcher before separate with property"], -); -test( - `${ruleName}: ✅ Good - dispatcher AFTER (chained) with property`, - tests["dispatcher after chained with property"], -); -test( - `${ruleName}: ✅ Good - dispatcher AFTER (separate) with property`, - tests["dispatcher after separate with property"], -); -test( - `${ruleName}: ❌ Bad - dispatcher configured, property missing`, - tests["dispatcher configured property missing"], -); -test( - `${ruleName}: ❌ Bad - dispatcher BEFORE (separate), property missing`, - tests["dispatcher before separate property missing"], -); -test( - `${ruleName}: ❌ Bad - dispatcher AFTER (separate), property missing`, - tests["dispatcher after separate property missing"], -); -test( - `${ruleName}: ❌ Bad - variable assignment without property`, - tests["variable assignment without property"], -); - -// Edge case tests -const edgeCases = createRequiredEdgeCaseTests( - "featuredTags", - config, - "setFeaturedTagsDispatcher", -); -test( - `${ruleName}: ✅ Edge - ternary with property in both branches`, - edgeCases["ternary with property in both branches"], -); -test( - `${ruleName}: ❌ Edge - ternary missing property in consequent`, - edgeCases["ternary missing property in consequent"], -); -test( - `${ruleName}: ❌ Edge - ternary missing property in alternate`, - edgeCases["ternary missing property in alternate"], -); -test( - `${ruleName}: ❌ Edge - ternary missing property in both branches`, - edgeCases["ternary missing property in both branches"], -); -test( - `${ruleName}: ✅ Edge - nested ternary with property`, - edgeCases["nested ternary with property"], -); -test( - `${ruleName}: ✅ Edge - if/else with property in both branches`, - edgeCases["if else with property in both branches"], -); -test( - `${ruleName}: ❌ Edge - if/else missing property in if block`, - edgeCases["if else missing property in if block"], -); -test( - `${ruleName}: ❌ Edge - if/else missing property in else block`, - edgeCases["if else missing property in else block"], -); -test( - `${ruleName}: ❌ Edge - if/else missing property in both blocks`, - edgeCases["if else missing property in both blocks"], -); -test( - `${ruleName}: ✅ Edge - nested if with property`, - edgeCases["nested if with property"], -); -test( - `${ruleName}: ✅ Edge - if else if else with featuredTags in all branches`, - edgeCases["if else if else with property in all branches"], -); -test( - `${ruleName}: ❌ Edge - if else if else missing featuredTags in else if`, - edgeCases["if else if else missing property in else if"], -); -test( - `${ruleName}: ✅ Edge - if else if with final return featuredTags in all paths`, - edgeCases["if else if with final return property in all paths"], -); -test( - `${ruleName}: ❌ Edge - if else if with final return missing featuredTags in final return`, - edgeCases["if else if with final return missing property in final return"], +runTests(ruleName, createRequiredDispatcherRuleTests("featuredTags", config)); +runTests( + ruleName, + createRequiredEdgeCaseTests( + "featuredTags", + config, + ), ); diff --git a/packages/lint/src/tests/actor-followers-property-mismatch.test.ts b/packages/lint/src/tests/actor-followers-property-mismatch.test.ts index d7c4c0829..3f111591b 100644 --- a/packages/lint/src/tests/actor-followers-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-followers-property-mismatch.test.ts @@ -1,87 +1,13 @@ -import { test } from "node:test"; import { RULE_IDS } from "../lib/const.ts"; import { createMismatchEdgeCaseTests, createMismatchRuleTests, + runTests, } from "../lib/test-templates.ts"; import * as rule from "../rules/actor-followers-property-mismatch.ts"; const ruleName = RULE_IDS.actorFollowersPropertyMismatch; - const config = { rule, ruleName }; -// Standard mismatch rule tests -const tests = createMismatchRuleTests("followers", config); -test( - `${ruleName}: ✅ Good - non-Federation object`, - tests["non-federation object"], -); -test( - `${ruleName}: ✅ Good - correct getter used`, - tests["correct getter used"], -); -test( - `${ruleName}: ✅ Good - property not present`, - tests["property not present"], -); -test(`${ruleName}: ❌ Bad - wrong getter used`, tests["wrong getter used"]); -test(`${ruleName}: ❌ Bad - wrong identifier`, tests["wrong identifier"]); - -// Edge case tests -const edgeCases = createMismatchEdgeCaseTests("followers", config); -test( - `${ruleName}: ✅ Edge - ternary with correct getter in both branches`, - edgeCases["ternary with correct getter in both branches"], -); -test( - `${ruleName}: ❌ Edge - ternary with wrong getter in consequent`, - edgeCases["ternary with wrong getter in consequent"], -); -test( - `${ruleName}: ❌ Edge - ternary with wrong getter in alternate`, - edgeCases["ternary with wrong getter in alternate"], -); -test( - `${ruleName}: ❌ Edge - ternary with wrong getter in both branches`, - edgeCases["ternary with wrong getter in both branches"], -); -test( - `${ruleName}: ✅ Edge - nested ternary with correct getter`, - edgeCases["nested ternary with correct getter"], -); -test( - `${ruleName}: ✅ Edge - if/else with correct getter in both branches`, - edgeCases["if else with correct getter in both branches"], -); -test( - `${ruleName}: ❌ Edge - if/else with wrong getter in if block`, - edgeCases["if else with wrong getter in if block"], -); -test( - `${ruleName}: ❌ Edge - if/else with wrong getter in else block`, - edgeCases["if else with wrong getter in else block"], -); -test( - `${ruleName}: ❌ Edge - if/else with wrong getter in both blocks`, - edgeCases["if else with wrong getter in both blocks"], -); -test( - `${ruleName}: ✅ Edge - nested if with correct getter`, - edgeCases["nested if with correct getter"], -); -test( - `${ruleName}: ✅ Edge - if else if else with correct getter in all branches`, - edgeCases["if else if else with correct getter in all branches"], -); -test( - `${ruleName}: ❌ Edge - if else if else with wrong getter in else if`, - edgeCases["if else if else with wrong getter in else if"], -); -test( - `${ruleName}: ✅ Edge - if else if with final return correct getter in all paths`, - edgeCases["if else if with final return correct getter in all paths"], -); -test( - `${ruleName}: ❌ Edge - if else if with final return wrong getter in final return`, - edgeCases["if else if with final return wrong getter in final return"], -); +runTests(ruleName, createMismatchRuleTests("followers", config)); +runTests(ruleName, createMismatchEdgeCaseTests("followers", config)); diff --git a/packages/lint/src/tests/actor-followers-property-required.test.ts b/packages/lint/src/tests/actor-followers-property-required.test.ts index 5370497f2..dac8f1f47 100644 --- a/packages/lint/src/tests/actor-followers-property-required.test.ts +++ b/packages/lint/src/tests/actor-followers-property-required.test.ts @@ -1,117 +1,16 @@ -import { test } from "node:test"; import { RULE_IDS } from "../lib/const.ts"; import { createRequiredDispatcherRuleTests, createRequiredEdgeCaseTests, + runTests, } from "../lib/test-templates.ts"; import * as rule from "../rules/actor-followers-property-required.ts"; const ruleName = RULE_IDS.actorFollowersPropertyRequired; - const config = { rule, ruleName }; -// Standard required dispatcher rule tests -const tests = createRequiredDispatcherRuleTests("followers", config); -test( - `${ruleName}: ✅ Good - non-Federation object`, - tests["non-federation object"], -); -test( - `${ruleName}: ✅ Good - dispatcher NOT configured`, - tests["dispatcher not configured"], -); -test( - `${ruleName}: ✅ Good - dispatcher BEFORE (chained) with property`, - tests["dispatcher before chained with property"], -); -test( - `${ruleName}: ✅ Good - dispatcher BEFORE (separate) with property`, - tests["dispatcher before separate with property"], -); -test( - `${ruleName}: ✅ Good - dispatcher AFTER (chained) with property`, - tests["dispatcher after chained with property"], -); -test( - `${ruleName}: ✅ Good - dispatcher AFTER (separate) with property`, - tests["dispatcher after separate with property"], -); -test( - `${ruleName}: ❌ Bad - dispatcher configured, property missing`, - tests["dispatcher configured property missing"], -); -test( - `${ruleName}: ❌ Bad - dispatcher BEFORE (separate), property missing`, - tests["dispatcher before separate property missing"], -); -test( - `${ruleName}: ❌ Bad - dispatcher AFTER (separate), property missing`, - tests["dispatcher after separate property missing"], -); -test( - `${ruleName}: ❌ Bad - variable assignment without property`, - tests["variable assignment without property"], -); - -// Edge case tests -const edgeCases = createRequiredEdgeCaseTests( - "followers", - config, - "setFollowersDispatcher", -); -test( - `${ruleName}: ✅ Edge - ternary with property in both branches`, - edgeCases["ternary with property in both branches"], -); -test( - `${ruleName}: ❌ Edge - ternary missing property in consequent`, - edgeCases["ternary missing property in consequent"], -); -test( - `${ruleName}: ❌ Edge - ternary missing property in alternate`, - edgeCases["ternary missing property in alternate"], -); -test( - `${ruleName}: ❌ Edge - ternary missing property in both branches`, - edgeCases["ternary missing property in both branches"], -); -test( - `${ruleName}: ✅ Edge - nested ternary with property`, - edgeCases["nested ternary with property"], -); -test( - `${ruleName}: ✅ Edge - if/else with property in both branches`, - edgeCases["if else with property in both branches"], -); -test( - `${ruleName}: ❌ Edge - if/else missing property in if block`, - edgeCases["if else missing property in if block"], -); -test( - `${ruleName}: ❌ Edge - if/else missing property in else block`, - edgeCases["if else missing property in else block"], -); -test( - `${ruleName}: ❌ Edge - if/else missing property in both blocks`, - edgeCases["if else missing property in both blocks"], -); -test( - `${ruleName}: ✅ Edge - nested if with property`, - edgeCases["nested if with property"], -); -test( - `${ruleName}: ✅ Edge - if else if else with followers in all branches`, - edgeCases["if else if else with property in all branches"], -); -test( - `${ruleName}: ❌ Edge - if else if else missing followers in else if`, - edgeCases["if else if else missing property in else if"], -); -test( - `${ruleName}: ✅ Edge - if else if with final return followers in all paths`, - edgeCases["if else if with final return property in all paths"], -); -test( - `${ruleName}: ❌ Edge - if else if with final return missing followers in final return`, - edgeCases["if else if with final return missing property in final return"], +runTests(ruleName, createRequiredDispatcherRuleTests("followers", config)); +runTests( + ruleName, + createRequiredEdgeCaseTests("followers", config), ); diff --git a/packages/lint/src/tests/actor-following-property-mismatch.test.ts b/packages/lint/src/tests/actor-following-property-mismatch.test.ts index e7fe65d37..29c78bdd0 100644 --- a/packages/lint/src/tests/actor-following-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-following-property-mismatch.test.ts @@ -1,87 +1,13 @@ -import { test } from "node:test"; import { RULE_IDS } from "../lib/const.ts"; import { createMismatchEdgeCaseTests, createMismatchRuleTests, + runTests, } from "../lib/test-templates.ts"; import * as rule from "../rules/actor-following-property-mismatch.ts"; const ruleName = RULE_IDS.actorFollowingPropertyMismatch; - const config = { rule, ruleName }; -// Standard mismatch rule tests -const tests = createMismatchRuleTests("following", config); -test( - `${ruleName}: ✅ Good - non-Federation object`, - tests["non-federation object"], -); -test( - `${ruleName}: ✅ Good - correct getter used`, - tests["correct getter used"], -); -test( - `${ruleName}: ✅ Good - property not present`, - tests["property not present"], -); -test(`${ruleName}: ❌ Bad - wrong getter used`, tests["wrong getter used"]); -test(`${ruleName}: ❌ Bad - wrong identifier`, tests["wrong identifier"]); - -// Edge case tests -const edgeCases = createMismatchEdgeCaseTests("following", config); -test( - `${ruleName}: ✅ Edge - ternary with correct getter in both branches`, - edgeCases["ternary with correct getter in both branches"], -); -test( - `${ruleName}: ❌ Edge - ternary with wrong getter in consequent`, - edgeCases["ternary with wrong getter in consequent"], -); -test( - `${ruleName}: ❌ Edge - ternary with wrong getter in alternate`, - edgeCases["ternary with wrong getter in alternate"], -); -test( - `${ruleName}: ❌ Edge - ternary with wrong getter in both branches`, - edgeCases["ternary with wrong getter in both branches"], -); -test( - `${ruleName}: ✅ Edge - nested ternary with correct getter`, - edgeCases["nested ternary with correct getter"], -); -test( - `${ruleName}: ✅ Edge - if/else with correct getter in both branches`, - edgeCases["if else with correct getter in both branches"], -); -test( - `${ruleName}: ❌ Edge - if/else with wrong getter in if block`, - edgeCases["if else with wrong getter in if block"], -); -test( - `${ruleName}: ❌ Edge - if/else with wrong getter in else block`, - edgeCases["if else with wrong getter in else block"], -); -test( - `${ruleName}: ❌ Edge - if/else with wrong getter in both blocks`, - edgeCases["if else with wrong getter in both blocks"], -); -test( - `${ruleName}: ✅ Edge - nested if with correct getter`, - edgeCases["nested if with correct getter"], -); -test( - `${ruleName}: ✅ Edge - if else if else with correct getter in all branches`, - edgeCases["if else if else with correct getter in all branches"], -); -test( - `${ruleName}: ❌ Edge - if else if else with wrong getter in else if`, - edgeCases["if else if else with wrong getter in else if"], -); -test( - `${ruleName}: ✅ Edge - if else if with final return correct getter in all paths`, - edgeCases["if else if with final return correct getter in all paths"], -); -test( - `${ruleName}: ❌ Edge - if else if with final return wrong getter in final return`, - edgeCases["if else if with final return wrong getter in final return"], -); +runTests(ruleName, createMismatchRuleTests("following", config)); +runTests(ruleName, createMismatchEdgeCaseTests("following", config)); diff --git a/packages/lint/src/tests/actor-following-property-required.test.ts b/packages/lint/src/tests/actor-following-property-required.test.ts index 03855ed4a..a1c9bbf45 100644 --- a/packages/lint/src/tests/actor-following-property-required.test.ts +++ b/packages/lint/src/tests/actor-following-property-required.test.ts @@ -1,117 +1,16 @@ -import { test } from "node:test"; import { RULE_IDS } from "../lib/const.ts"; import { createRequiredDispatcherRuleTests, createRequiredEdgeCaseTests, + runTests, } from "../lib/test-templates.ts"; import * as rule from "../rules/actor-following-property-required.ts"; const ruleName = RULE_IDS.actorFollowingPropertyRequired; - const config = { rule, ruleName }; -// Standard required dispatcher rule tests -const tests = createRequiredDispatcherRuleTests("following", config); -test( - `${ruleName}: ✅ Good - non-Federation object`, - tests["non-federation object"], -); -test( - `${ruleName}: ✅ Good - dispatcher NOT configured`, - tests["dispatcher not configured"], -); -test( - `${ruleName}: ✅ Good - dispatcher BEFORE (chained) with property`, - tests["dispatcher before chained with property"], -); -test( - `${ruleName}: ✅ Good - dispatcher BEFORE (separate) with property`, - tests["dispatcher before separate with property"], -); -test( - `${ruleName}: ✅ Good - dispatcher AFTER (chained) with property`, - tests["dispatcher after chained with property"], -); -test( - `${ruleName}: ✅ Good - dispatcher AFTER (separate) with property`, - tests["dispatcher after separate with property"], -); -test( - `${ruleName}: ❌ Bad - dispatcher configured, property missing`, - tests["dispatcher configured property missing"], -); -test( - `${ruleName}: ❌ Bad - dispatcher BEFORE (separate), property missing`, - tests["dispatcher before separate property missing"], -); -test( - `${ruleName}: ❌ Bad - dispatcher AFTER (separate), property missing`, - tests["dispatcher after separate property missing"], -); -test( - `${ruleName}: ❌ Bad - variable assignment without property`, - tests["variable assignment without property"], -); - -// Edge case tests -const edgeCases = createRequiredEdgeCaseTests( - "following", - config, - "setFollowingDispatcher", -); -test( - `${ruleName}: ✅ Edge - ternary with property in both branches`, - edgeCases["ternary with property in both branches"], -); -test( - `${ruleName}: ❌ Edge - ternary missing property in consequent`, - edgeCases["ternary missing property in consequent"], -); -test( - `${ruleName}: ❌ Edge - ternary missing property in alternate`, - edgeCases["ternary missing property in alternate"], -); -test( - `${ruleName}: ❌ Edge - ternary missing property in both branches`, - edgeCases["ternary missing property in both branches"], -); -test( - `${ruleName}: ✅ Edge - nested ternary with property`, - edgeCases["nested ternary with property"], -); -test( - `${ruleName}: ✅ Edge - if/else with property in both branches`, - edgeCases["if else with property in both branches"], -); -test( - `${ruleName}: ❌ Edge - if/else missing property in if block`, - edgeCases["if else missing property in if block"], -); -test( - `${ruleName}: ❌ Edge - if/else missing property in else block`, - edgeCases["if else missing property in else block"], -); -test( - `${ruleName}: ❌ Edge - if/else missing property in both blocks`, - edgeCases["if else missing property in both blocks"], -); -test( - `${ruleName}: ✅ Edge - nested if with property`, - edgeCases["nested if with property"], -); -test( - `${ruleName}: ✅ Edge - if else if else with following in all branches`, - edgeCases["if else if else with property in all branches"], -); -test( - `${ruleName}: ❌ Edge - if else if else missing following in else if`, - edgeCases["if else if else missing property in else if"], -); -test( - `${ruleName}: ✅ Edge - if else if with final return following in all paths`, - edgeCases["if else if with final return property in all paths"], -); -test( - `${ruleName}: ❌ Edge - if else if with final return missing following in final return`, - edgeCases["if else if with final return missing property in final return"], +runTests(ruleName, createRequiredDispatcherRuleTests("following", config)); +runTests( + ruleName, + createRequiredEdgeCaseTests("following", config), ); diff --git a/packages/lint/src/tests/actor-id-mismatch.test.ts b/packages/lint/src/tests/actor-id-mismatch.test.ts index e11960a8a..b6f06a77b 100644 --- a/packages/lint/src/tests/actor-id-mismatch.test.ts +++ b/packages/lint/src/tests/actor-id-mismatch.test.ts @@ -1,96 +1,13 @@ -import { test } from "node:test"; import { RULE_IDS } from "../lib/const.ts"; import { createIdMismatchEdgeCaseTests, createIdMismatchRuleTests, + runTests, } from "../lib/test-templates.ts"; import * as rule from "../rules/actor-id-mismatch.ts"; const ruleName = RULE_IDS.actorIdMismatch; const config = { rule, ruleName }; -// Standard id mismatch rule tests -const tests = createIdMismatchRuleTests(config); -test( - `${ruleName}: ✅ Good - non-federation object`, - tests["non-federation object"], -); -test( - `${ruleName}: ✅ Good - correct getter used`, - tests["correct getter used"], -); -test( - `${ruleName}: ❌ Bad - literal string id`, - tests["literal string id"], -); -test( - `${ruleName}: ❌ Bad - new URL as id`, - tests["new URL as id"], -); -test( - `${ruleName}: ❌ Bad - wrong getter used`, - tests["wrong getter used"], -); -test( - `${ruleName}: ❌ Bad - wrong identifier`, - tests["wrong identifier"], -); - -// Edge case tests -const edgeCases = createIdMismatchEdgeCaseTests(config); -test( - `${ruleName}: ✅ Edge - ternary with correct getter in both branches`, - edgeCases["ternary with correct getter in both branches"], -); -test( - `${ruleName}: ❌ Edge - ternary with wrong getter in consequent`, - edgeCases["ternary with wrong getter in consequent"], -); -test( - `${ruleName}: ❌ Edge - ternary with wrong getter in alternate`, - edgeCases["ternary with wrong getter in alternate"], -); -test( - `${ruleName}: ❌ Edge - ternary with wrong getter in both branches`, - edgeCases["ternary with wrong getter in both branches"], -); -test( - `${ruleName}: ✅ Edge - nested ternary with correct getter`, - edgeCases["nested ternary with correct getter"], -); -test( - `${ruleName}: ✅ Edge - if/else with correct getter in both branches`, - edgeCases["if else with correct getter in both branches"], -); -test( - `${ruleName}: ❌ Edge - if/else with wrong getter in if block`, - edgeCases["if else with wrong getter in if block"], -); -test( - `${ruleName}: ❌ Edge - if/else with wrong getter in else block`, - edgeCases["if else with wrong getter in else block"], -); -test( - `${ruleName}: ❌ Edge - if/else with wrong getter in both blocks`, - edgeCases["if else with wrong getter in both blocks"], -); -test( - `${ruleName}: ✅ Edge - nested if with correct getter`, - edgeCases["nested if with correct getter"], -); -test( - `${ruleName}: ✅ Edge - if else if else with correct getter in all branches`, - edgeCases["if else if else with correct getter in all branches"], -); -test( - `${ruleName}: ❌ Edge - if else if else with wrong getter in else if`, - edgeCases["if else if else with wrong getter in else if"], -); -test( - `${ruleName}: ✅ Edge - if else if with final return correct getter in all paths`, - edgeCases["if else if with final return correct getter in all paths"], -); -test( - `${ruleName}: ❌ Edge - if else if with final return wrong getter in final return`, - edgeCases["if else if with final return wrong getter in final return"], -); +runTests(ruleName, createIdMismatchRuleTests(config)); +runTests(ruleName, createIdMismatchEdgeCaseTests(config)); diff --git a/packages/lint/src/tests/actor-id-required.test.ts b/packages/lint/src/tests/actor-id-required.test.ts index 52fe38572..de6c2a8b5 100644 --- a/packages/lint/src/tests/actor-id-required.test.ts +++ b/packages/lint/src/tests/actor-id-required.test.ts @@ -1,101 +1,13 @@ -import { test } from "node:test"; import { RULE_IDS } from "../lib/const.ts"; import { createIdRequiredEdgeCaseTests, createIdRequiredRuleTests, + runTests, } from "../lib/test-templates.ts"; import * as rule from "../rules/actor-id-required.ts"; const ruleName = RULE_IDS.actorIdRequired; const config = { rule, ruleName }; -// Standard id required rule tests -const tests = createIdRequiredRuleTests(config); -test( - `${ruleName}: ✅ Good - non-federation object`, - tests["non-federation object"], -); -test( - `${ruleName}: ✅ Good - with id property any value`, - tests["with id property any value"], -); -test( - `${ruleName}: ✅ Good - with id property using getActorUri`, - tests["with id property using getActorUri"], -); -test( - `${ruleName}: ✅ Good - block statement with id`, - tests["block statement with id"], -); -test(`${ruleName}: ❌ Bad - without id property`, tests["without id property"]); -test( - `${ruleName}: ❌ Bad - returning empty object`, - tests["returning empty object"], -); -test( - `${ruleName}: ✅ Good - multiple properties including id`, - tests["multiple properties including id"], -); -test( - `${ruleName}: ❌ Bad - variable assignment without id`, - tests["variable assignment without id"], -); - -// Edge case tests -const edgeCases = createIdRequiredEdgeCaseTests(config); -test( - `${ruleName}: ✅ Edge - ternary with id in both branches`, - edgeCases["ternary with id in both branches"], -); -test( - `${ruleName}: ❌ Edge - ternary missing id in consequent`, - edgeCases["ternary missing id in consequent"], -); -test( - `${ruleName}: ❌ Edge - ternary missing id in alternate`, - edgeCases["ternary missing id in alternate"], -); -test( - `${ruleName}: ❌ Edge - ternary missing id in both branches`, - edgeCases["ternary missing id in both branches"], -); -test( - `${ruleName}: ✅ Edge - nested ternary with id`, - edgeCases["nested ternary with id"], -); -test( - `${ruleName}: ✅ Edge - if/else with id in both branches`, - edgeCases["if else with id in both branches"], -); -test( - `${ruleName}: ❌ Edge - if/else missing id in if block`, - edgeCases["if else missing id in if block"], -); -test( - `${ruleName}: ❌ Edge - if/else missing id in else block`, - edgeCases["if else missing id in else block"], -); -test( - `${ruleName}: ❌ Edge - if/else missing id in both blocks`, - edgeCases["if else missing id in both blocks"], -); -test( - `${ruleName}: ✅ Edge - nested if with id`, - edgeCases["nested if with id"], -); -test( - `${ruleName}: ✅ Edge - if else if else with id in all branches`, - edgeCases["if else if else with id in all branches"], -); -test( - `${ruleName}: ❌ Edge - if else if else missing id in else if`, - edgeCases["if else if else missing id in else if"], -); -test( - `${ruleName}: ✅ Edge - if else if with final return id in all paths`, - edgeCases["if else if with final return id in all paths"], -); -test( - `${ruleName}: ❌ Edge - if else if with final return missing id in final return`, - edgeCases["if else if with final return missing id in final return"], -); +runTests(ruleName, createIdRequiredRuleTests(config)); +runTests(ruleName, createIdRequiredEdgeCaseTests(config)); diff --git a/packages/lint/src/tests/actor-inbox-property-mismatch.test.ts b/packages/lint/src/tests/actor-inbox-property-mismatch.test.ts index 5b1918926..b3494d192 100644 --- a/packages/lint/src/tests/actor-inbox-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-inbox-property-mismatch.test.ts @@ -1,87 +1,13 @@ -import { test } from "node:test"; import { RULE_IDS } from "../lib/const.ts"; import { createMismatchEdgeCaseTests, createMismatchRuleTests, + runTests, } from "../lib/test-templates.ts"; import * as rule from "../rules/actor-inbox-property-mismatch.ts"; const ruleName = RULE_IDS.actorInboxPropertyMismatch; - const config = { rule, ruleName }; -// Standard mismatch rule tests -const tests = createMismatchRuleTests("inbox", config); -test( - `${ruleName}: ✅ Good - non-Federation object`, - tests["non-federation object"], -); -test( - `${ruleName}: ✅ Good - correct getter used`, - tests["correct getter used"], -); -test( - `${ruleName}: ✅ Good - property not present`, - tests["property not present"], -); -test(`${ruleName}: ❌ Bad - wrong getter used`, tests["wrong getter used"]); -test(`${ruleName}: ❌ Bad - wrong identifier`, tests["wrong identifier"]); - -// Edge case tests -const edgeCases = createMismatchEdgeCaseTests("inbox", config); -test( - `${ruleName}: ✅ Edge - ternary with correct getter in both branches`, - edgeCases["ternary with correct getter in both branches"], -); -test( - `${ruleName}: ❌ Edge - ternary with wrong getter in consequent`, - edgeCases["ternary with wrong getter in consequent"], -); -test( - `${ruleName}: ❌ Edge - ternary with wrong getter in alternate`, - edgeCases["ternary with wrong getter in alternate"], -); -test( - `${ruleName}: ❌ Edge - ternary with wrong getter in both branches`, - edgeCases["ternary with wrong getter in both branches"], -); -test( - `${ruleName}: ✅ Edge - nested ternary with correct getter`, - edgeCases["nested ternary with correct getter"], -); -test( - `${ruleName}: ✅ Edge - if/else with correct getter in both branches`, - edgeCases["if else with correct getter in both branches"], -); -test( - `${ruleName}: ❌ Edge - if/else with wrong getter in if block`, - edgeCases["if else with wrong getter in if block"], -); -test( - `${ruleName}: ❌ Edge - if/else with wrong getter in else block`, - edgeCases["if else with wrong getter in else block"], -); -test( - `${ruleName}: ❌ Edge - if/else with wrong getter in both blocks`, - edgeCases["if else with wrong getter in both blocks"], -); -test( - `${ruleName}: ✅ Edge - nested if with correct getter`, - edgeCases["nested if with correct getter"], -); -test( - `${ruleName}: ✅ Edge - if else if else with correct getter in all branches`, - edgeCases["if else if else with correct getter in all branches"], -); -test( - `${ruleName}: ❌ Edge - if else if else with wrong getter in else if`, - edgeCases["if else if else with wrong getter in else if"], -); -test( - `${ruleName}: ✅ Edge - if else if with final return correct getter in all paths`, - edgeCases["if else if with final return correct getter in all paths"], -); -test( - `${ruleName}: ❌ Edge - if else if with final return wrong getter in final return`, - edgeCases["if else if with final return wrong getter in final return"], -); +runTests(ruleName, createMismatchRuleTests("inbox", config)); +runTests(ruleName, createMismatchEdgeCaseTests("inbox", config)); diff --git a/packages/lint/src/tests/actor-inbox-property-required.test.ts b/packages/lint/src/tests/actor-inbox-property-required.test.ts index 543b3215f..4f91fb021 100644 --- a/packages/lint/src/tests/actor-inbox-property-required.test.ts +++ b/packages/lint/src/tests/actor-inbox-property-required.test.ts @@ -1,8 +1,8 @@ -import { test } from "node:test"; import { RULE_IDS } from "../lib/const.ts"; import { + createRequiredDispatcherRuleTests, createRequiredEdgeCaseTests, - createRequiredListenerRuleTests, + runTests, } from "../lib/test-templates.ts"; import * as rule from "../rules/actor-inbox-property-required.ts"; @@ -11,107 +11,13 @@ const ruleName = RULE_IDS.actorInboxPropertyRequired; const config = { rule, ruleName }; // Standard required listener rule tests -const tests = createRequiredListenerRuleTests("inbox", config); -test( - `${ruleName}: ✅ Good - non-Federation object`, - tests["non-federation object"], -); -test( - `${ruleName}: ✅ Good - listeners NOT configured`, - tests["listeners not configured"], -); -test( - `${ruleName}: ✅ Good - listeners BEFORE (chained) with property`, - tests["listeners before chained with property"], -); -test( - `${ruleName}: ✅ Good - listeners BEFORE (separate) with property`, - tests["listeners before separate with property"], -); -test( - `${ruleName}: ✅ Good - listeners AFTER (chained) with property`, - tests["listeners after chained with property"], -); -test( - `${ruleName}: ✅ Good - listeners AFTER (separate) with property`, - tests["listeners after separate with property"], -); -test( - `${ruleName}: ❌ Bad - listeners configured, property missing`, - tests["listeners configured property missing"], -); -test( - `${ruleName}: ❌ Bad - listeners BEFORE (separate), property missing`, - tests["listeners before separate property missing"], -); -test( - `${ruleName}: ❌ Bad - listeners AFTER (separate), property missing`, - tests["listeners after separate property missing"], -); -test( - `${ruleName}: ❌ Bad - variable assignment without property`, - tests["variable assignment without property"], +runTests( + ruleName, + createRequiredDispatcherRuleTests("inbox", config), ); // Edge case tests -const edgeCases = createRequiredEdgeCaseTests( - "inbox", - config, - "setInboxListeners", -); -test( - `${ruleName}: ✅ Edge - ternary with property in both branches`, - edgeCases["ternary with property in both branches"], -); -test( - `${ruleName}: ❌ Edge - ternary missing property in consequent`, - edgeCases["ternary missing property in consequent"], -); -test( - `${ruleName}: ❌ Edge - ternary missing property in alternate`, - edgeCases["ternary missing property in alternate"], -); -test( - `${ruleName}: ❌ Edge - ternary missing property in both branches`, - edgeCases["ternary missing property in both branches"], -); -test( - `${ruleName}: ✅ Edge - nested ternary with property`, - edgeCases["nested ternary with property"], -); -test( - `${ruleName}: ✅ Edge - if/else with property in both branches`, - edgeCases["if else with property in both branches"], -); -test( - `${ruleName}: ❌ Edge - if/else missing property in if block`, - edgeCases["if else missing property in if block"], -); -test( - `${ruleName}: ❌ Edge - if/else missing property in else block`, - edgeCases["if else missing property in else block"], -); -test( - `${ruleName}: ❌ Edge - if/else missing property in both blocks`, - edgeCases["if else missing property in both blocks"], -); -test( - `${ruleName}: ✅ Edge - nested if with property`, - edgeCases["nested if with property"], -); -test( - `${ruleName}: ✅ Edge - if else if else with inbox in all branches`, - edgeCases["if else if else with property in all branches"], -); -test( - `${ruleName}: ❌ Edge - if else if else missing inbox in else if`, - edgeCases["if else if else missing property in else if"], -); -test( - `${ruleName}: ✅ Edge - if else if with final return inbox in all paths`, - edgeCases["if else if with final return property in all paths"], -); -test( - `${ruleName}: ❌ Edge - if else if with final return missing inbox in final return`, - edgeCases["if else if with final return missing property in final return"], +runTests( + ruleName, + createRequiredEdgeCaseTests("inbox", config), ); diff --git a/packages/lint/src/tests/actor-liked-property-mismatch.test.ts b/packages/lint/src/tests/actor-liked-property-mismatch.test.ts index 443fdffc0..9631cf0b0 100644 --- a/packages/lint/src/tests/actor-liked-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-liked-property-mismatch.test.ts @@ -1,87 +1,13 @@ -import { test } from "node:test"; import { RULE_IDS } from "../lib/const.ts"; import { createMismatchEdgeCaseTests, createMismatchRuleTests, + runTests, } from "../lib/test-templates.ts"; import * as rule from "../rules/actor-liked-property-mismatch.ts"; const ruleName = RULE_IDS.actorLikedPropertyMismatch; - const config = { rule, ruleName }; -// Standard mismatch rule tests -const tests = createMismatchRuleTests("liked", config); -test( - `${ruleName}: ✅ Good - non-Federation object`, - tests["non-federation object"], -); -test( - `${ruleName}: ✅ Good - correct getter used`, - tests["correct getter used"], -); -test( - `${ruleName}: ✅ Good - property not present`, - tests["property not present"], -); -test(`${ruleName}: ❌ Bad - wrong getter used`, tests["wrong getter used"]); -test(`${ruleName}: ❌ Bad - wrong identifier`, tests["wrong identifier"]); - -// Edge case tests -const edgeCases = createMismatchEdgeCaseTests("liked", config); -test( - `${ruleName}: ✅ Edge - ternary with correct getter in both branches`, - edgeCases["ternary with correct getter in both branches"], -); -test( - `${ruleName}: ❌ Edge - ternary with wrong getter in consequent`, - edgeCases["ternary with wrong getter in consequent"], -); -test( - `${ruleName}: ❌ Edge - ternary with wrong getter in alternate`, - edgeCases["ternary with wrong getter in alternate"], -); -test( - `${ruleName}: ❌ Edge - ternary with wrong getter in both branches`, - edgeCases["ternary with wrong getter in both branches"], -); -test( - `${ruleName}: ✅ Edge - nested ternary with correct getter`, - edgeCases["nested ternary with correct getter"], -); -test( - `${ruleName}: ✅ Edge - if/else with correct getter in both branches`, - edgeCases["if else with correct getter in both branches"], -); -test( - `${ruleName}: ❌ Edge - if/else with wrong getter in if block`, - edgeCases["if else with wrong getter in if block"], -); -test( - `${ruleName}: ❌ Edge - if/else with wrong getter in else block`, - edgeCases["if else with wrong getter in else block"], -); -test( - `${ruleName}: ❌ Edge - if/else with wrong getter in both blocks`, - edgeCases["if else with wrong getter in both blocks"], -); -test( - `${ruleName}: ✅ Edge - nested if with correct getter`, - edgeCases["nested if with correct getter"], -); -test( - `${ruleName}: ✅ Edge - if else if else with correct getter in all branches`, - edgeCases["if else if else with correct getter in all branches"], -); -test( - `${ruleName}: ❌ Edge - if else if else with wrong getter in else if`, - edgeCases["if else if else with wrong getter in else if"], -); -test( - `${ruleName}: ✅ Edge - if else if with final return correct getter in all paths`, - edgeCases["if else if with final return correct getter in all paths"], -); -test( - `${ruleName}: ❌ Edge - if else if with final return wrong getter in final return`, - edgeCases["if else if with final return wrong getter in final return"], -); +runTests(ruleName, createMismatchRuleTests("liked", config)); +runTests(ruleName, createMismatchEdgeCaseTests("liked", config)); diff --git a/packages/lint/src/tests/actor-liked-property-required.test.ts b/packages/lint/src/tests/actor-liked-property-required.test.ts index 41a150f8c..710874de6 100644 --- a/packages/lint/src/tests/actor-liked-property-required.test.ts +++ b/packages/lint/src/tests/actor-liked-property-required.test.ts @@ -1,117 +1,16 @@ -import { test } from "node:test"; import { RULE_IDS } from "../lib/const.ts"; import { createRequiredDispatcherRuleTests, createRequiredEdgeCaseTests, + runTests, } from "../lib/test-templates.ts"; import * as rule from "../rules/actor-liked-property-required.ts"; const ruleName = RULE_IDS.actorLikedPropertyRequired; - const config = { rule, ruleName }; -// Standard required dispatcher rule tests -const tests = createRequiredDispatcherRuleTests("liked", config); -test( - `${ruleName}: ✅ Good - non-Federation object`, - tests["non-federation object"], -); -test( - `${ruleName}: ✅ Good - dispatcher NOT configured`, - tests["dispatcher not configured"], -); -test( - `${ruleName}: ✅ Good - dispatcher BEFORE (chained) with property`, - tests["dispatcher before chained with property"], -); -test( - `${ruleName}: ✅ Good - dispatcher BEFORE (separate) with property`, - tests["dispatcher before separate with property"], -); -test( - `${ruleName}: ✅ Good - dispatcher AFTER (chained) with property`, - tests["dispatcher after chained with property"], -); -test( - `${ruleName}: ✅ Good - dispatcher AFTER (separate) with property`, - tests["dispatcher after separate with property"], -); -test( - `${ruleName}: ❌ Bad - dispatcher configured, property missing`, - tests["dispatcher configured property missing"], -); -test( - `${ruleName}: ❌ Bad - dispatcher BEFORE (separate), property missing`, - tests["dispatcher before separate property missing"], -); -test( - `${ruleName}: ❌ Bad - dispatcher AFTER (separate), property missing`, - tests["dispatcher after separate property missing"], -); -test( - `${ruleName}: ❌ Bad - variable assignment without property`, - tests["variable assignment without property"], -); - -// Edge case tests -const edgeCases = createRequiredEdgeCaseTests( - "liked", - config, - "setLikedDispatcher", -); -test( - `${ruleName}: ✅ Edge - ternary with property in both branches`, - edgeCases["ternary with property in both branches"], -); -test( - `${ruleName}: ❌ Edge - ternary missing property in consequent`, - edgeCases["ternary missing property in consequent"], -); -test( - `${ruleName}: ❌ Edge - ternary missing property in alternate`, - edgeCases["ternary missing property in alternate"], -); -test( - `${ruleName}: ❌ Edge - ternary missing property in both branches`, - edgeCases["ternary missing property in both branches"], -); -test( - `${ruleName}: ✅ Edge - nested ternary with property`, - edgeCases["nested ternary with property"], -); -test( - `${ruleName}: ✅ Edge - if/else with property in both branches`, - edgeCases["if else with property in both branches"], -); -test( - `${ruleName}: ❌ Edge - if/else missing property in if block`, - edgeCases["if else missing property in if block"], -); -test( - `${ruleName}: ❌ Edge - if/else missing property in else block`, - edgeCases["if else missing property in else block"], -); -test( - `${ruleName}: ❌ Edge - if/else missing property in both blocks`, - edgeCases["if else missing property in both blocks"], -); -test( - `${ruleName}: ✅ Edge - nested if with property`, - edgeCases["nested if with property"], -); -test( - `${ruleName}: ✅ Edge - if else if else with liked in all branches`, - edgeCases["if else if else with property in all branches"], -); -test( - `${ruleName}: ❌ Edge - if else if else missing liked in else if`, - edgeCases["if else if else missing property in else if"], -); -test( - `${ruleName}: ✅ Edge - if else if with final return liked in all paths`, - edgeCases["if else if with final return property in all paths"], -); -test( - `${ruleName}: ❌ Edge - if else if with final return missing liked in final return`, - edgeCases["if else if with final return missing property in final return"], +runTests(ruleName, createRequiredDispatcherRuleTests("liked", config)); +runTests( + ruleName, + createRequiredEdgeCaseTests("liked", config), ); diff --git a/packages/lint/src/tests/actor-outbox-property-mismatch.test.ts b/packages/lint/src/tests/actor-outbox-property-mismatch.test.ts index 5f1c8a38f..690f24ee3 100644 --- a/packages/lint/src/tests/actor-outbox-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-outbox-property-mismatch.test.ts @@ -1,87 +1,13 @@ -import { test } from "node:test"; import { RULE_IDS } from "../lib/const.ts"; import { createMismatchEdgeCaseTests, createMismatchRuleTests, + runTests, } from "../lib/test-templates.ts"; import * as rule from "../rules/actor-outbox-property-mismatch.ts"; const ruleName = RULE_IDS.actorOutboxPropertyMismatch; - const config = { rule, ruleName }; -// Standard mismatch rule tests -const tests = createMismatchRuleTests("outbox", config); -test( - `${ruleName}: ✅ Good - non-Federation object`, - tests["non-federation object"], -); -test( - `${ruleName}: ✅ Good - correct getter used`, - tests["correct getter used"], -); -test( - `${ruleName}: ✅ Good - property not present`, - tests["property not present"], -); -test(`${ruleName}: ❌ Bad - wrong getter used`, tests["wrong getter used"]); -test(`${ruleName}: ❌ Bad - wrong identifier`, tests["wrong identifier"]); - -// Edge case tests -const edgeCases = createMismatchEdgeCaseTests("outbox", config); -test( - `${ruleName}: ✅ Edge - ternary with correct getter in both branches`, - edgeCases["ternary with correct getter in both branches"], -); -test( - `${ruleName}: ❌ Edge - ternary with wrong getter in consequent`, - edgeCases["ternary with wrong getter in consequent"], -); -test( - `${ruleName}: ❌ Edge - ternary with wrong getter in alternate`, - edgeCases["ternary with wrong getter in alternate"], -); -test( - `${ruleName}: ❌ Edge - ternary with wrong getter in both branches`, - edgeCases["ternary with wrong getter in both branches"], -); -test( - `${ruleName}: ✅ Edge - nested ternary with correct getter`, - edgeCases["nested ternary with correct getter"], -); -test( - `${ruleName}: ✅ Edge - if/else with correct getter in both branches`, - edgeCases["if else with correct getter in both branches"], -); -test( - `${ruleName}: ❌ Edge - if/else with wrong getter in if block`, - edgeCases["if else with wrong getter in if block"], -); -test( - `${ruleName}: ❌ Edge - if/else with wrong getter in else block`, - edgeCases["if else with wrong getter in else block"], -); -test( - `${ruleName}: ❌ Edge - if/else with wrong getter in both blocks`, - edgeCases["if else with wrong getter in both blocks"], -); -test( - `${ruleName}: ✅ Edge - nested if with correct getter`, - edgeCases["nested if with correct getter"], -); -test( - `${ruleName}: ✅ Edge - if else if else with correct getter in all branches`, - edgeCases["if else if else with correct getter in all branches"], -); -test( - `${ruleName}: ❌ Edge - if else if else with wrong getter in else if`, - edgeCases["if else if else with wrong getter in else if"], -); -test( - `${ruleName}: ✅ Edge - if else if with final return correct getter in all paths`, - edgeCases["if else if with final return correct getter in all paths"], -); -test( - `${ruleName}: ❌ Edge - if else if with final return wrong getter in final return`, - edgeCases["if else if with final return wrong getter in final return"], -); +runTests(ruleName, createMismatchRuleTests("outbox", config)); +runTests(ruleName, createMismatchEdgeCaseTests("outbox", config)); diff --git a/packages/lint/src/tests/actor-outbox-property-required.test.ts b/packages/lint/src/tests/actor-outbox-property-required.test.ts index b1945c7e8..dc2f287c6 100644 --- a/packages/lint/src/tests/actor-outbox-property-required.test.ts +++ b/packages/lint/src/tests/actor-outbox-property-required.test.ts @@ -1,117 +1,16 @@ -import { test } from "node:test"; import { RULE_IDS } from "../lib/const.ts"; import { createRequiredDispatcherRuleTests, createRequiredEdgeCaseTests, + runTests, } from "../lib/test-templates.ts"; import * as rule from "../rules/actor-outbox-property-required.ts"; const ruleName = RULE_IDS.actorOutboxPropertyRequired; - const config = { rule, ruleName }; -// Standard required dispatcher rule tests -const tests = createRequiredDispatcherRuleTests("outbox", config); -test( - `${ruleName}: ✅ Good - non-Federation object`, - tests["non-federation object"], -); -test( - `${ruleName}: ✅ Good - dispatcher NOT configured`, - tests["dispatcher not configured"], -); -test( - `${ruleName}: ✅ Good - dispatcher BEFORE (chained) with property`, - tests["dispatcher before chained with property"], -); -test( - `${ruleName}: ✅ Good - dispatcher BEFORE (separate) with property`, - tests["dispatcher before separate with property"], -); -test( - `${ruleName}: ✅ Good - dispatcher AFTER (chained) with property`, - tests["dispatcher after chained with property"], -); -test( - `${ruleName}: ✅ Good - dispatcher AFTER (separate) with property`, - tests["dispatcher after separate with property"], -); -test( - `${ruleName}: ❌ Bad - dispatcher configured, property missing`, - tests["dispatcher configured property missing"], -); -test( - `${ruleName}: ❌ Bad - dispatcher BEFORE (separate), property missing`, - tests["dispatcher before separate property missing"], -); -test( - `${ruleName}: ❌ Bad - dispatcher AFTER (separate), property missing`, - tests["dispatcher after separate property missing"], -); -test( - `${ruleName}: ❌ Bad - variable assignment without property`, - tests["variable assignment without property"], -); - -// Edge case tests -const edgeCases = createRequiredEdgeCaseTests( - "outbox", - config, - "setOutboxDispatcher", -); -test( - `${ruleName}: ✅ Edge - ternary with property in both branches`, - edgeCases["ternary with property in both branches"], -); -test( - `${ruleName}: ❌ Edge - ternary missing property in consequent`, - edgeCases["ternary missing property in consequent"], -); -test( - `${ruleName}: ❌ Edge - ternary missing property in alternate`, - edgeCases["ternary missing property in alternate"], -); -test( - `${ruleName}: ❌ Edge - ternary missing property in both branches`, - edgeCases["ternary missing property in both branches"], -); -test( - `${ruleName}: ✅ Edge - nested ternary with property`, - edgeCases["nested ternary with property"], -); -test( - `${ruleName}: ✅ Edge - if/else with property in both branches`, - edgeCases["if else with property in both branches"], -); -test( - `${ruleName}: ❌ Edge - if/else missing property in if block`, - edgeCases["if else missing property in if block"], -); -test( - `${ruleName}: ❌ Edge - if/else missing property in else block`, - edgeCases["if else missing property in else block"], -); -test( - `${ruleName}: ❌ Edge - if/else missing property in both blocks`, - edgeCases["if else missing property in both blocks"], -); -test( - `${ruleName}: ✅ Edge - nested if with property`, - edgeCases["nested if with property"], -); -test( - `${ruleName}: ✅ Edge - if else if else with outbox in all branches`, - edgeCases["if else if else with property in all branches"], -); -test( - `${ruleName}: ❌ Edge - if else if else missing outbox in else if`, - edgeCases["if else if else missing property in else if"], -); -test( - `${ruleName}: ✅ Edge - if else if with final return outbox in all paths`, - edgeCases["if else if with final return property in all paths"], -); -test( - `${ruleName}: ❌ Edge - if else if with final return missing outbox in final return`, - edgeCases["if else if with final return missing property in final return"], +runTests(ruleName, createRequiredDispatcherRuleTests("outbox", config)); +runTests( + ruleName, + createRequiredEdgeCaseTests("outbox", config), ); diff --git a/packages/lint/src/tests/actor-public-key-required.test.ts b/packages/lint/src/tests/actor-public-key-required.test.ts index 5cae2f495..fabe92402 100644 --- a/packages/lint/src/tests/actor-public-key-required.test.ts +++ b/packages/lint/src/tests/actor-public-key-required.test.ts @@ -1,255 +1,13 @@ -import { test } from "node:test"; -import { properties, RULE_IDS } from "../lib/const.ts"; -import { actorPropertyRequired } from "../lib/messages.ts"; -import { createKeyRequiredEdgeCaseTests } from "../lib/test-templates.ts"; -import lintTest from "../lib/test.ts"; +import { RULE_IDS } from "../lib/const.ts"; +import { + createRequiredDispatcherRuleTests, + createRequiredEdgeCaseTests, + runTests, +} from "../lib/test-templates.ts"; import * as rule from "../rules/actor-public-key-required.ts"; const ruleName = RULE_IDS.actorPublicKeyRequired; - -test( - `${ruleName}: ✅ Good - \`setActorDispatcher\` called on non-Federation object`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ - name: "John Doe", - }); - }); - `, - rule, - ruleName, - federationSetup: ` - const federation = { - setActorDispatcher: () => {} - }; - `, - }), -); - -test( - `${ruleName}: ✅ Good - key pairs dispatcher NOT configured, property missing (no error)`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ - id: ctx.getActorUri(identifier), - name: "Alice", - inbox: ctx.getInboxUri(identifier), - }); - }); - `, - rule, - ruleName, - }), -); - -test( - `${ruleName}: ✅ Good - key pairs dispatcher configured BEFORE setActorDispatcher, property present`, - lintTest({ - code: ` - federation - .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - const keyPairs = await ctx.getActorKeyPairs(identifier); - return new Person({ - id: ctx.getActorUri(identifier), - publicKey: keyPairs[0].cryptographicKey, - }); - }) - .setKeyPairsDispatcher(async (ctx, identifier) => []); - `, - rule, - ruleName, - }), -); - -test( - `${ruleName}: ✅ Good - key pairs dispatcher configured BEFORE setActorDispatcher (separate calls), property present`, - lintTest({ - code: ` - federation.setKeyPairsDispatcher(async (ctx, identifier) => []); - - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - const keyPairs = await ctx.getActorKeyPairs(identifier); - return new Person({ - id: ctx.getActorUri(identifier), - publicKey: keyPairs[0].cryptographicKey, - }); - }); - `, - rule, - ruleName, - }), -); - -test( - `${ruleName}: ✅ Good - key pairs dispatcher configured AFTER setActorDispatcher, property present`, - lintTest({ - code: ` - federation - .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - const keyPairs = await ctx.getActorKeyPairs(identifier); - return new Person({ - id: ctx.getActorUri(identifier), - publicKey: keyPairs[0].cryptographicKey, - }); - }) - .setKeyPairsDispatcher(async (ctx, identifier) => []); - `, - rule, - ruleName, - }), -); - -test( - `${ruleName}: ✅ Good - key pairs dispatcher configured AFTER setActorDispatcher (separate calls), property present`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - const keyPairs = await ctx.getActorKeyPairs(identifier); - return new Person({ - id: ctx.getActorUri(identifier), - publicKey: keyPairs[0].cryptographicKey, - }); - }); - - federation.setKeyPairsDispatcher(async (ctx, identifier) => []); - `, - rule, - ruleName, - }), -); - -test( - `${ruleName}: ❌ Bad - key pairs dispatcher configured BEFORE, property missing`, - lintTest({ - code: ` - federation - .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ - id: ctx.getActorUri(identifier), - name: "Alice", - }); - }) - .setKeyPairsDispatcher(async (ctx, identifier) => []); - `, - rule, - ruleName, - expectedError: actorPropertyRequired(properties.publicKey), - }), -); - -test( - `${ruleName}: ❌ Bad - key pairs dispatcher configured BEFORE (separate calls), property missing`, - lintTest({ - code: ` - federation.setKeyPairsDispatcher(async (ctx, identifier) => []); - - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ - id: ctx.getActorUri(identifier), - name: "Alice", - }); - }); - `, - rule, - ruleName, - expectedError: actorPropertyRequired(properties.publicKey), - }), -); - -test( - `${ruleName}: ❌ Bad - key pairs dispatcher configured AFTER, property missing`, - lintTest({ - code: ` - federation - .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ - id: ctx.getActorUri(identifier), - }); - }) - .setKeyPairsDispatcher(async (ctx, identifier) => []); - `, - rule, - ruleName, - expectedError: actorPropertyRequired(properties.publicKey), - }), -); - -test( - `${ruleName}: ❌ Bad - key pairs dispatcher configured AFTER (separate calls), property missing`, - lintTest({ - code: ` - federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ - id: ctx.getActorUri(identifier), - }); - }); - - federation.setKeyPairsDispatcher(async (ctx, identifier) => []); - `, - rule, - ruleName, - expectedError: actorPropertyRequired(properties.publicKey), - }), -); - -// Edge case tests const config = { rule, ruleName }; -const edgeCases = createKeyRequiredEdgeCaseTests("publicKey", config); -test( - `${ruleName}: ✅ Edge - ternary with property in both branches`, - edgeCases["ternary with property in both branches"], -); -test( - `${ruleName}: ❌ Edge - ternary missing property in consequent`, - edgeCases["ternary missing property in consequent"], -); -test( - `${ruleName}: ❌ Edge - ternary missing property in alternate`, - edgeCases["ternary missing property in alternate"], -); -test( - `${ruleName}: ❌ Edge - ternary missing property in both branches`, - edgeCases["ternary missing property in both branches"], -); -test( - `${ruleName}: ✅ Edge - nested ternary with property`, - edgeCases["nested ternary with property"], -); -test( - `${ruleName}: ✅ Edge - if/else with property in both branches`, - edgeCases["if else with property in both branches"], -); -test( - `${ruleName}: ❌ Edge - if/else missing property in if block`, - edgeCases["if else missing property in if block"], -); -test( - `${ruleName}: ❌ Edge - if/else missing property in else block`, - edgeCases["if else missing property in else block"], -); -test( - `${ruleName}: ❌ Edge - if/else missing property in both blocks`, - edgeCases["if else missing property in both blocks"], -); -test( - `${ruleName}: ✅ Edge - nested if with property`, - edgeCases["nested if with property"], -); -test( - `${ruleName}: ✅ Edge - if else if else with property in all branches`, - edgeCases["if else if else with property in all branches"], -); -test( - `${ruleName}: ❌ Edge - if else if else missing property in else if`, - edgeCases["if else if else missing property in else if"], -); -test( - `${ruleName}: ✅ Edge - if else if with final return property in all paths`, - edgeCases["if else if with final return property in all paths"], -); -test( - `${ruleName}: ❌ Edge - if else if with final return missing property in final return`, - edgeCases["if else if with final return missing property in final return"], -); + +runTests(ruleName, createRequiredDispatcherRuleTests("publicKey", config)); +runTests(ruleName, createRequiredEdgeCaseTests("publicKey", config)); diff --git a/packages/lint/src/tests/actor-shared-inbox-property-mismatch.test.ts b/packages/lint/src/tests/actor-shared-inbox-property-mismatch.test.ts index 6d9c2d153..ce961c65e 100644 --- a/packages/lint/src/tests/actor-shared-inbox-property-mismatch.test.ts +++ b/packages/lint/src/tests/actor-shared-inbox-property-mismatch.test.ts @@ -1,87 +1,13 @@ -import { test } from "node:test"; import { RULE_IDS } from "../lib/const.ts"; import { createMismatchEdgeCaseTests, createMismatchRuleTests, + runTests, } from "../lib/test-templates.ts"; import * as rule from "../rules/actor-shared-inbox-property-mismatch.ts"; const ruleName = RULE_IDS.actorSharedInboxPropertyMismatch; - const config = { rule, ruleName }; -// Standard mismatch rule tests -const tests = createMismatchRuleTests("sharedInbox", config); -test( - `${ruleName}: ✅ Good - non-Federation object`, - tests["non-federation object"], -); -test( - `${ruleName}: ✅ Good - correct getter used`, - tests["correct getter used"], -); -test( - `${ruleName}: ✅ Good - property not present`, - tests["property not present"], -); -test(`${ruleName}: ❌ Bad - wrong getter used`, tests["wrong getter used"]); -test(`${ruleName}: ❌ Bad - wrong identifier`, tests["wrong identifier"]); - -// Edge case tests -const edgeCases = createMismatchEdgeCaseTests("sharedInbox", config); -test( - `${ruleName}: ✅ Edge - ternary with correct getter in both branches`, - edgeCases["ternary with correct getter in both branches"], -); -test( - `${ruleName}: ❌ Edge - ternary with wrong getter in consequent`, - edgeCases["ternary with wrong getter in consequent"], -); -test( - `${ruleName}: ❌ Edge - ternary with wrong getter in alternate`, - edgeCases["ternary with wrong getter in alternate"], -); -test( - `${ruleName}: ❌ Edge - ternary with wrong getter in both branches`, - edgeCases["ternary with wrong getter in both branches"], -); -test( - `${ruleName}: ✅ Edge - nested ternary with correct getter`, - edgeCases["nested ternary with correct getter"], -); -test( - `${ruleName}: ✅ Edge - if/else with correct getter in both branches`, - edgeCases["if else with correct getter in both branches"], -); -test( - `${ruleName}: ❌ Edge - if/else with wrong getter in if block`, - edgeCases["if else with wrong getter in if block"], -); -test( - `${ruleName}: ❌ Edge - if/else with wrong getter in else block`, - edgeCases["if else with wrong getter in else block"], -); -test( - `${ruleName}: ❌ Edge - if/else with wrong getter in both blocks`, - edgeCases["if else with wrong getter in both blocks"], -); -test( - `${ruleName}: ✅ Edge - nested if with correct getter`, - edgeCases["nested if with correct getter"], -); -test( - `${ruleName}: ✅ Edge - if else if else with correct getter in all branches`, - edgeCases["if else if else with correct getter in all branches"], -); -test( - `${ruleName}: ❌ Edge - if else if else with wrong getter in else if`, - edgeCases["if else if else with wrong getter in else if"], -); -test( - `${ruleName}: ✅ Edge - if else if with final return correct getter in all paths`, - edgeCases["if else if with final return correct getter in all paths"], -); -test( - `${ruleName}: ❌ Edge - if else if with final return wrong getter in final return`, - edgeCases["if else if with final return wrong getter in final return"], -); +runTests(ruleName, createMismatchRuleTests("sharedInbox", config)); +runTests(ruleName, createMismatchEdgeCaseTests("sharedInbox", config)); diff --git a/packages/lint/src/tests/actor-shared-inbox-property-required.test.ts b/packages/lint/src/tests/actor-shared-inbox-property-required.test.ts index b332b1bd1..c45a5599e 100644 --- a/packages/lint/src/tests/actor-shared-inbox-property-required.test.ts +++ b/packages/lint/src/tests/actor-shared-inbox-property-required.test.ts @@ -1,8 +1,8 @@ -import { test } from "node:test"; import { RULE_IDS } from "../lib/const.ts"; import { + createRequiredDispatcherRuleTests, createRequiredEdgeCaseTests, - createRequiredListenerRuleTests, + runTests, } from "../lib/test-templates.ts"; import * as rule from "../rules/actor-shared-inbox-property-required.ts"; @@ -11,107 +11,13 @@ const ruleName = RULE_IDS.actorSharedInboxPropertyRequired; const config = { rule, ruleName }; // Standard required listener rule tests -const tests = createRequiredListenerRuleTests("sharedInbox", config); -test( - `${ruleName}: ✅ Good - non-Federation object`, - tests["non-federation object"], -); -test( - `${ruleName}: ✅ Good - listeners NOT configured`, - tests["listeners not configured"], -); -test( - `${ruleName}: ✅ Good - listeners BEFORE (chained) with property`, - tests["listeners before chained with property"], -); -test( - `${ruleName}: ✅ Good - listeners BEFORE (separate) with property`, - tests["listeners before separate with property"], -); -test( - `${ruleName}: ✅ Good - listeners AFTER (chained) with property`, - tests["listeners after chained with property"], -); -test( - `${ruleName}: ✅ Good - listeners AFTER (separate) with property`, - tests["listeners after separate with property"], -); -test( - `${ruleName}: ❌ Bad - listeners configured, property missing`, - tests["listeners configured property missing"], -); -test( - `${ruleName}: ❌ Bad - listeners BEFORE (separate), property missing`, - tests["listeners before separate property missing"], -); -test( - `${ruleName}: ❌ Bad - listeners AFTER (separate), property missing`, - tests["listeners after separate property missing"], -); -test( - `${ruleName}: ❌ Bad - variable assignment without property`, - tests["variable assignment without property"], +runTests( + ruleName, + createRequiredDispatcherRuleTests("sharedInbox", config), ); // Edge case tests -const edgeCases = createRequiredEdgeCaseTests( - "sharedInbox", - config, - "setInboxListeners", -); -test( - `${ruleName}: ✅ Edge - ternary with property in both branches`, - edgeCases["ternary with property in both branches"], -); -test( - `${ruleName}: ❌ Edge - ternary missing property in consequent`, - edgeCases["ternary missing property in consequent"], -); -test( - `${ruleName}: ❌ Edge - ternary missing property in alternate`, - edgeCases["ternary missing property in alternate"], -); -test( - `${ruleName}: ❌ Edge - ternary missing property in both branches`, - edgeCases["ternary missing property in both branches"], -); -test( - `${ruleName}: ✅ Edge - nested ternary with property`, - edgeCases["nested ternary with property"], -); -test( - `${ruleName}: ✅ Edge - if/else with property in both branches`, - edgeCases["if else with property in both branches"], -); -test( - `${ruleName}: ❌ Edge - if/else missing property in if block`, - edgeCases["if else missing property in if block"], -); -test( - `${ruleName}: ❌ Edge - if/else missing property in else block`, - edgeCases["if else missing property in else block"], -); -test( - `${ruleName}: ❌ Edge - if/else missing property in both blocks`, - edgeCases["if else missing property in both blocks"], -); -test( - `${ruleName}: ✅ Edge - nested if with property`, - edgeCases["nested if with property"], -); -test( - `${ruleName}: ✅ Edge - if else if else with sharedInbox in all branches`, - edgeCases["if else if else with property in all branches"], -); -test( - `${ruleName}: ❌ Edge - if else if else missing sharedInbox in else if`, - edgeCases["if else if else missing property in else if"], -); -test( - `${ruleName}: ✅ Edge - if else if with final return sharedInbox in all paths`, - edgeCases["if else if with final return property in all paths"], -); -test( - `${ruleName}: ❌ Edge - if else if with final return missing sharedInbox in final return`, - edgeCases["if else if with final return missing property in final return"], +runTests( + ruleName, + createRequiredEdgeCaseTests("sharedInbox", config), ); From 72e52109d94b6c8eff262ec541d14157aae4950d Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Wed, 17 Dec 2025 06:31:41 +0000 Subject: [PATCH 35/41] Added documents about `@fedify/lint` --- AGENTS.md | 1 + CHANGES.md | 7 + docs/tutorial/basics.md | 122 ++++++++++++ packages/lint/README.md | 410 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 540 insertions(+) create mode 100644 packages/lint/README.md diff --git a/AGENTS.md b/AGENTS.md index b51bed607..d534224ba 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -77,6 +77,7 @@ The repository is organized as a monorepo with the following packages: - *packages/koa/*: Koa integration (@fedify/koa) - *packages/postgres/*: PostgreSQL drivers (@fedify/postgres) - *packages/redis/*: Redis drivers (@fedify/redis) + - *packages/lint/*: Linting utilities (@fedify/lint) - *packages/nestjs/*: NestJS integration (@fedify/nestjs) - *packages/next/*: Next.js integration (@fedify/next) - *packages/sqlite/*: SQLite driver (@fedify/sqlite) diff --git a/CHANGES.md b/CHANGES.md index 3b9ab3245..36e7c17dd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -186,6 +186,13 @@ To be released. - This package is primarily used by generated vocabulary classes and provides the runtime infrastructure for ActivityPub object processing. +### @fedify/lint + + - Created Fedify linting tools as the *@fedify/lint* package. + This package provides shared Deno Lint and ESLint configurations for + consistent code style across Fedify packages and user projects. + [[#297], [#494] by ChanHaeng Lee] + Version 1.10.0 -------------- diff --git a/docs/tutorial/basics.md b/docs/tutorial/basics.md index 2d64b2736..e6f532576 100644 --- a/docs/tutorial/basics.md +++ b/docs/tutorial/basics.md @@ -1279,6 +1279,128 @@ should see the actor's followers in the bulleted list. [Hono]: https://hono.dev/ +Linting your federation code +----------------------------- + +As your federated server grows, it's important to maintain code quality and +catch common mistakes early. Fedify provides linting plugins for both +[Deno Lint] and [ESLint], which include specialized linting rules for +federation code. + +The `@fedify/lint` package helps you avoid common pitfalls such as: + + - Using incorrect actor IDs (e.g., relative URLs instead of + `Context.getActorUri()`) + - Missing required actor properties (inbox, outbox, followers, etc.) + - Incorrect URL patterns for actor collections + - Missing public keys or assertion methods + +### Using Deno Lint + +If you are using Deno, you can use the Deno Lint plugin. First, install the +plugin: + +~~~~ sh +deno add jsr:@fedify/lint +~~~~ + +Then add the plugin to your *deno.json* configuration file: + +~~~~ json +{ + "lint": { + "plugins": ["jsr:@fedify/lint"] + } +} +~~~~ + +Now you can run the linter on your code: + +~~~~ sh +deno lint +~~~~ + +### Using ESLint + +For ESLint users (including Deno, Bun, and Node.js), first install the plugin: + +::: code-group + +~~~~ sh [Bun] +bun add -D @fedify/lint eslint +~~~~ + +~~~~ sh [Node.js] +npm add -D @fedify/lint eslint +~~~~ + +::: + +Then create an ESLint configuration file (e.g., *eslint.config.ts*) in your +project directory: + +~~~~ typescript twoslash +import fedifyLint from "@fedify/lint"; + +export default [{ + ...fedifyLint, + files: ["**/*.ts"], +}]; +~~~~ + +Now you can run the linter on your code: + +::: code-group + +~~~~ sh [Bun] +bun run eslint . +~~~~ + +~~~~ sh [Node.js] +npx eslint . +~~~~ + +::: + +> [!NOTE] +> When using ESLint with Deno, you may need to exclude some Deno-specific +> lint rules like `no-unused-vars` to avoid conflicts. + +### Example + +For example, if you had written the actor dispatcher like this: + +~~~~ typescript +// ❌ Wrong: Using relative URL +federation.setActorDispatcher( + "/{identifier}", + (_ctx, identifier) => { + return new Person({ + id: new URL(`/${identifier}`), // ❌ Linter will catch this + name: "Example User", + }); + }, +); +~~~~ + +The linter would warn you: + +~~~~ +error[fedify-lint/actor-id-mismatch]: Actor's `id` property must match +`_ctx.getActorUri(identifier)`. Ensure you're using the correct context method. +~~~~ + +This helps you catch mistakes before they cause issues in production. The +linter includes many other rules to ensure your federation code follows best +practices. + +For more information about available rules and configuration options, see +the [`@fedify/lint` documentation](https://jsr.io/@fedify/lint). + +[Deno Lint]: https://docs.deno.com/runtime/reference/lint_plugins/ +[ESLint]: https://eslint.org/ + + Wrapping up ----------- diff --git a/packages/lint/README.md b/packages/lint/README.md new file mode 100644 index 000000000..fd9385a56 --- /dev/null +++ b/packages/lint/README.md @@ -0,0 +1,410 @@ + + +@fedify/lint: ESLint plugin for Fedify +======================================= + +[![JSR][JSR badge]][JSR] +[![npm][npm badge]][npm] +[![Follow @fedify@hollo.social][@fedify@hollo.social badge]][@fedify@hollo.social] + +*This package is available since Fedify 2.0.0.* + +This package provides [Deno Lint] and [ESLint] plugin with lint rules specifically designed +for [Fedify] applications. It helps you catch common mistakes and enforce +best practices when building federated server apps with Fedify. + +The plugin includes rules that check for: + + - Proper actor ID configuration + - Required actor properties (inbox, outbox, followers, etc.) + - Correct URL patterns for actor collections + - Public key and assertion method requirements + - Collection filtering implementation + + +### Deno Lint Configuration Example + +~~~~ typescript +// deno.json + +{ + "lint": { + "plugins": { + "@fedify/lint": "jsr:@fedify/lint" + }, + "rules": { + "@fedify/lint/actor-id-required": "error", + "@fedify/lint/actor-id-mismatch": "error", + "@fedify/lint/actor-inbox-property-required": "warn" + // ... other rules + } + } +} +~~~~ + +### ESLint Configuration Example + +~~~~ typescript +// eslint.config.ts + +import fedifyLint from "@fedify/lint"; + +export default fedifyLint; +~~~~ + +[JSR]: https://jsr.io/@fedify/lint +[JSR badge]: https://jsr.io/badges/@fedify/lint +[npm]: https://www.npmjs.com/package/@fedify/lint +[npm badge]: https://img.shields.io/npm/v/@fedify/lint?logo=npm +[@fedify@hollo.social badge]: https://fedi-badge.deno.dev/@fedify@hollo.social/followers.svg +[@fedify@hollo.social]: https://hollo.social/@fedify +[Deno Lint]: https://docs.deno.com/runtime/reference/lint_plugins/ +[ESLint]: https://eslint.org/ +[Fedify]: https://fedify.dev/ + + +Features +-------- + +The `@fedify/lint` package provides comprehensive linting rules for Fedify +federation code: + +### Actor Validation Rules + + - **`actor-id-required`**: Ensures all actors have an `id` property + - **`actor-id-mismatch`**: Validates that actor IDs match the expected URI + from `Context.getActorUri()` + - **`actor-public-key-required`**: Ensures actors have public keys for + HTTP Signatures + - **`actor-assertion-method-required`**: Validates assertion methods for + Object Integrity Proofs + +### Collection Property Rules + + - **`actor-inbox-property-required`**: Ensures inbox is defined when + `setInboxListeners` is set + - **`actor-inbox-property-mismatch`**: Validates inbox URI from `getInboxUri` + - **`actor-outbox-property-required`**: Ensures outbox is defined when + `setOutboxDispatcher` is set + - **`actor-outbox-property-mismatch`**: Validates outbox URI from + `getOutboxUri` + - **`actor-followers-property-required`**: Ensures followers is defined when + `setFollowersDispatcher` is set + - **`actor-followers-property-mismatch`**: Validates followers URI from + `getFollowersUri` + - **`actor-following-property-required`**: Ensures following is defined when + `setFollowingDispatcher` is set + - **`actor-following-property-mismatch`**: Validates following URI from + `getFollowingUri` + - **`actor-liked-property-required`**: Ensures liked is defined when + `setLikedDispatcher` is set + - **`actor-liked-property-mismatch`**: Validates liked URI from `getLikedUri` + - **`actor-featured-property-required`**: Ensures featured is defined when + `setFeaturedDispatcher` is set + - **`actor-featured-property-mismatch`**: Validates featured URI from + `getFeaturedUri` + - **`actor-featured-tags-property-required`**: Ensures featuredTags is defined + when `setFeaturedTagsDispatcher` is set + - **`actor-featured-tags-property-mismatch`**: Validates featuredTags URI from + `getFeaturedTagsUri` + - **`actor-shared-inbox-property-required`**: Ensures sharedInbox is defined + when `setInboxListeners` is set + - **`actor-shared-inbox-property-mismatch`**: Validates sharedInbox URI from + `getInboxUri` + +### Other Rules + + - **`collection-filtering-not-implemented`**: Warns about missing collection + filtering implementation (`setFollowersDispatcher` only for now) + + +Installation +------------ + +::: code-group + +~~~~ sh [Deno] +deno add jsr:@fedify/lint +~~~~ + +~~~~ sh [npm] +npm add -D @fedify/lint +~~~~ + +~~~~ sh [pnpm] +pnpm add -D @fedify/lint +~~~~ + +~~~~ sh [Yarn] +yarn add -D @fedify/lint +~~~~ + +~~~~ sh [Bun] +bun add -D @fedify/lint +~~~~ + +::: + + +Usage (Deno Lint) +------------------ + +### Basic Setup + +Add the plugin to your *deno.json* configuration file: + +~~~~ json +{ + "lint": { + "plugins": ["jsr:@fedify/lint"] + } +} +~~~~ + +By default, this enables all recommended rules. + +### Custom Configuration + +You can customize which rules to enable and their severity levels: + +~~~~ json +{ + "lint": { + "plugins": ["jsr:@fedify/lint"], + "rules": { + "tags": ["recommended"], + "include": [ + "@fedify/lint/actor-id-required", + "@fedify/lint/actor-id-mismatch" + ], + "exclude": [ + "@fedify/lint/actor-featured-property-required" + ] + } + } +} +~~~~ + +### Running Deno Lint + +After setting up the configuration, run Deno's linter: + +~~~~ sh +deno lint +~~~~ + +You can also specify which files to lint: + +~~~~ sh +deno lint federation.ts +deno lint src/federation/ +~~~~ + + +Usage (ESLint) +-------------- + +### Basic Setup + +Add the plugin to your ESLint configuration file (e.g., *eslint.config.ts* +or *eslint.config.js*): + +~~~~ typescript +import fedifyLint from "@fedify/lint"; + +// If your `createFederation` code is in `federation.ts` or `federation/**.ts` +export default fedifyLint; + +// Or specify your own federation files +export default { + ...fedifyLint, + files: ["my-own-federation.ts"], +}; + +// If you use other ESLint configurations + +export default [ + otherConfig, + fedifyLint, +]; +~~~~ + +The default configuration applies recommended rules to files that match +common federation-related patterns (e.g., *federation.ts*, *federation/\*.ts*). + +### Custom Configuration + +You can customize which files to lint and which rules to enable: + +~~~~ typescript +import { plugin } from "@fedify/lint"; + +export default [{ + files: ["src/federation/**/*.ts"], // Your federation code location + plugins: { + "@fedify/lint": plugin, + }, + rules: { + "@fedify/lint/actor-id-required": "error", + "@fedify/lint/actor-id-mismatch": "error", + "@fedify/lint/actor-inbox-property-required": "warn", + // ... other rules + }, +}]; +~~~~ + +### Using Configurations + +The plugin provides two preset configurations: + +#### Recommended (default) + +Enables critical rules as errors and optional rules as warnings: + +~~~~ typescript +import fedifyLint from "@fedify/lint"; + +export default fedifyLint; +~~~~ + +#### Strict + +Enables all rules as errors: + +~~~~ typescript +import { plugin } from "@fedify/lint"; + +export default [{ + files: ["**/*.ts"], + ...plugin.configs.strict, +}]; +~~~~ + + +Example +------- + +Here's an example of code that would trigger lint errors: + +~~~~ typescript +// ❌ Wrong: Using relative URL for actor ID +import { createFederation, Person } from "@fedify/fedify"; + +const federation = createFederation({ /* ... */ }); + +federation.setActorDispatcher( + "/{identifier}", + (_ctx, identifier) => { + return new Person({ + id: new URL(`/${identifier}`), // ❌ Should use ctx.getActorUri() + name: "Example User", + }); + }, +); +~~~~ + +Corrected version: + +~~~~ typescript +// ✅ Correct: Using Context.getActorUri() for actor ID +import { createFederation, Person } from "@fedify/fedify"; + +const federation = createFederation({ /* ... */ }); + +federation.setActorDispatcher( + "/{identifier}", + (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), // ✅ Correct + name: "Example User", + inbox: ctx.getInboxUri(identifier), + outbox: ctx.getOutboxUri(identifier), + followers: ctx.getFollowersUri(identifier), + // ... other required properties + }); + }, +); +~~~~ + + +Running the Linter +------------------ + +### Deno Lint + +Run Deno's linter with the plugin enabled: + +~~~~ sh +deno lint +~~~~ + +You can also specify which files or directories to lint: + +~~~~ sh +deno lint federation.ts +deno lint src/federation/ +~~~~ + +### ESLint + +Set up your ESLint configuration as shown above and add a follwing script on +`package.json`: + +~~~~ jsonc +{ + "scripts": { + "lint": "eslint ." + } +} +~~~~ + +After setting up the configuration, run ESLint on your codebase: + +::: code-group + +~~~~ sh [npm] +npm run lint +~~~~ + +~~~~ sh [pnpm] +pnpm lint +~~~~ + +~~~~ sh [Yarn] +yarn lint +~~~~ + +~~~~ sh [Bun] +bun lint +~~~~ + +::: + +or run the linter directly via command line: + +::: code-group + +~~~~ sh [npm] +npx eslint . +~~~~ + +~~~~ sh [pnpm] +pnpx eslint . +~~~~ + +~~~~ sh [Yarn] +yarn eslint . +~~~~ + +~~~~ sh [Bun] +bunx eslint . +~~~~ + +::: + +See Also +-------- + + - [Fedify Documentation](https://fedify.dev/) + - [ESLint Documentation](https://eslint.org/) + - [Example Project](https://github.com/fedify-dev/fedify/tree/main/examples/lint) From 5940b2150d65c79c865e6346acd9fea6b734a6fc Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Fri, 19 Dec 2025 11:23:49 +0000 Subject: [PATCH 36/41] Fixed infinite recursion --- packages/lint/src/lib/property-checker.ts | 71 ++++++++++++++++------- 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/packages/lint/src/lib/property-checker.ts b/packages/lint/src/lib/property-checker.ts index 1c00f890b..4a17e2c0c 100644 --- a/packages/lint/src/lib/property-checker.ts +++ b/packages/lint/src/lib/property-checker.ts @@ -11,7 +11,7 @@ import { unless, when, } from "@fxts/core"; -import { allOf, isNode, isNodeType } from "./pred.ts"; +import { allOf, isNodeType } from "./pred.ts"; import type { AssignmentPattern, BlockStatement, @@ -234,28 +234,57 @@ const collectReturnPaths = ( ); function* flatten(node: Node): Generator { - if (isNodeType("ReturnStatement")(node)) yield node; + switch (node.type) { + case "ReturnStatement": + yield node; + return; - if (isNodeType("IfStatement")(node)) { - // Collect returns from both branches - if (node.consequent) yield* flatten(node.consequent); - if (node.alternate) yield* flatten(node.alternate); - } + case "IfStatement": + // Collect returns from both branches + if (node.consequent) yield* flatten(node.consequent); + if (node.alternate) yield* flatten(node.alternate); + return; - if (isNodeType("BlockStatement")(node)) { - yield* node.body.map(flatten).flatMap(toArrayIfIter); - } + case "BlockStatement": + for (const statement of node.body) { + yield* flatten(statement); + } + return; - for (const child of Object.values(node)) { - if (isNode(child)) { - yield* flatten(child); - } else if (Array.isArray(child)) { - yield* child.filter(isNode).map(flatten).flatMap(toArrayIfIter); - } + case "SwitchStatement": + for (const switchCase of node.cases) { + for (const statement of switchCase.consequent) { + yield* flatten(statement); + } + } + return; + + case "TryStatement": + yield* flatten(node.block); + if (node.handler) yield* flatten(node.handler.body); + if (node.finalizer) yield* flatten(node.finalizer); + return; + + case "WhileStatement": + case "DoWhileStatement": + case "ForStatement": + case "ForInStatement": + case "ForOfStatement": + yield* flatten(node.body); + return; + + case "LabeledStatement": + yield* flatten(node.body); + return; + + case "WithStatement": + yield* flatten(node.body); + return; + + default: + // For other node types (expressions, declarations, etc.), + // we don't traverse deeper to avoid infinite recursion + // from circular references like `parent` + return; } } - -const toArrayIfIter = (input: T | Iterable): T[] => - Symbol.iterator in Object(input) - ? toArray(input as Iterable) - : [input as T]; From f7b1c90be3d3f7da43b36aa522da17607034f313 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Fri, 19 Dec 2025 11:24:14 +0000 Subject: [PATCH 37/41] Separated key required test --- packages/lint/src/lib/test-templates.ts | 1089 ++++++----------- .../actor-assertion-method-required.test.ts | 8 +- .../tests/actor-public-key-required.test.ts | 8 +- 3 files changed, 401 insertions(+), 704 deletions(-) diff --git a/packages/lint/src/lib/test-templates.ts b/packages/lint/src/lib/test-templates.ts index f2ac71242..8f1dca65a 100644 --- a/packages/lint/src/lib/test-templates.ts +++ b/packages/lint/src/lib/test-templates.ts @@ -37,7 +37,7 @@ export const runTests = (ruleName: string, tests: TestSuite) => consume, ); -const createDispatcherCode = (content: string) => ` +const createActorDispatcherCode = (content: string) => ` federation.setActorDispatcher( "/users/{identifier}", async (ctx, identifier) => { @@ -58,14 +58,14 @@ const createChainedDispatcherCode = ( .${dispatcherMethod}(async (ctx, identifier) => []); `; -const createSeparateDispatcherCode = ( +const createDispatcherCode = ( content: string, dispatcherMethod: string, - isBefore: boolean, + isBefore?: boolean | undefined, ) => { const dispatcher = `federation.${dispatcherMethod}(async (ctx, identifier) => []);`; - const actor = createDispatcherCode(content); + const actor = createActorDispatcherCode(content); return isBefore ? `${dispatcher}\n${actor}` : `${actor}\n${dispatcher}`; }; @@ -159,153 +159,107 @@ const createTestCode = ( * Creates required rule tests for standard properties that use a dispatcher * (following, followers, outbox, liked, featured, featuredTags) */ -export function createRequiredDispatcherRuleTests( - propertyKey: PropertyKey, - config: TestConfig, -): TestSuite { - const { rule, ruleName } = config; - const prop = properties[propertyKey]; - const expectedError = actorPropertyRequired(prop); - - return { - // ✅ Good - non-Federation object - "non-federation object": [ - lintTest({ - code: createDispatcherCode(createTestCode(propertyKey, false)), - rule, - ruleName, - federationSetup: ` +export const createRequiredDispatcherRuleTests = + requiredDispatcherRuleTestsFactory(createDispatcherCode); +export const createKeyRequiredDispatcherRuleTests = + requiredDispatcherRuleTestsFactory(createChainedDispatcherCode); + +function requiredDispatcherRuleTestsFactory( + createDispatcherCode: (content: string, dispatcherMethod: string) => string, +) { + return function ( + propertyKey: PropertyKey, + config: TestConfig, + ): TestSuite { + const { rule, ruleName } = config; + const prop = properties[propertyKey]; + const expectedError = actorPropertyRequired(prop); + + return { + // ✅ Good - non-Federation object + "non-federation object": [ + lintTest({ + code: createDispatcherCode( + createTestCode(propertyKey, false), + prop.setter, + ), + rule, + ruleName, + federationSetup: ` const federation = { setActorDispatcher: () => {} }; `, - }), - true, - ], - - // ✅ Good - dispatcher NOT configured - "dispatcher not configured": [ - lintTest({ - code: createDispatcherCode(createTestCode(propertyKey, false)), - rule, - ruleName, - }), - true, - ], - - // ✅ Good - dispatcher configured BEFORE (chained) - "dispatcher before chained with property": [ - lintTest({ - code: createChainedDispatcherCode( - createTestCode(propertyKey, true), - prop.setter, - ), - rule, - ruleName, - }), - true, - ], - - // ✅ Good - dispatcher configured BEFORE (separate) - "dispatcher before separate with property": [ - lintTest({ - code: createSeparateDispatcherCode( - createTestCode(propertyKey, true), - prop.setter, - true, - ), - rule, - ruleName, - }), - true, - ], - - // ✅ Good - dispatcher configured AFTER (chained) - "dispatcher after chained with property": [ - lintTest({ - code: createChainedDispatcherCode( - createTestCode(propertyKey, true), - prop.setter, - ), - rule, - ruleName, - }), - true, - ], - - // ✅ Good - dispatcher configured AFTER (separate) - "dispatcher after separate with property": [ - lintTest({ - code: createSeparateDispatcherCode( - createTestCode(propertyKey, true), - prop.setter, - false, - ), - rule, - ruleName, - }), - true, - ], - - // ❌ Bad - dispatcher configured, property missing - "dispatcher configured property missing": [ - lintTest({ - code: createChainedDispatcherCode( - createTestCode(propertyKey, false), - prop.setter, - ), - rule, - ruleName, - expectedError, - }), - false, - ], - - // ❌ Bad - dispatcher before (separate), property missing - "dispatcher before separate property missing": [ - lintTest({ - code: createSeparateDispatcherCode( - createTestCode(propertyKey, false), - prop.setter, - true, - ), - rule, - ruleName, - expectedError, - }), - false, - ], - - // ❌ Bad - dispatcher after (separate), property missing - "dispatcher after separate property missing": [ - lintTest({ - code: createSeparateDispatcherCode( - createTestCode(propertyKey, false), - prop.setter, - false, - ), - rule, - ruleName, - expectedError, - }), - false, - ], - - // ❌ Bad - variable assignment without property - "variable assignment without property": [ - lintTest({ - code: createChainedDispatcherCode( - `const actor = new Person({ - ${ID_PROP} - name: "John Doe", - }); - return actor;`, - prop.setter, - ), - rule, - ruleName, - expectedError, - }), - false, - ], + }), + true, + ], + + // ✅ Good - dispatcher NOT configured + "dispatcher not configured": [ + lintTest({ + code: createActorDispatcherCode(createTestCode(propertyKey, false)), + rule, + ruleName, + }), + true, + ], + + // ✅ Good - dispatcher configured BEFORE + "dispatcher before separate with property": [ + lintTest({ + code: createDispatcherCode( + createTestCode(propertyKey, true), + prop.setter, + true, + ), + rule, + ruleName, + }), + true, + ], + + // ✅ Good - dispatcher configured AFTER + "dispatcher after separate with property": [ + lintTest({ + code: createDispatcherCode( + createTestCode(propertyKey, true), + prop.setter, + false, + ), + rule, + ruleName, + }), + true, + ], + + // ❌ Bad - dispatcher before, property missing + "dispatcher before separate property missing": [ + lintTest({ + code: createDispatcherCode( + createTestCode(propertyKey, false), + prop.setter, + true, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ❌ Bad - dispatcher after, property missing + "dispatcher after separate property missing": [ + lintTest({ + code: createDispatcherCode( + createTestCode(propertyKey, false), + prop.setter, + false, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + }; }; } @@ -320,7 +274,9 @@ export function createIdRequiredRuleTests(config: TestConfig): TestSuite { // ✅ Good - non-Federation object "non-federation object": [ lintTest({ - code: createDispatcherCode(`return new Person({ name: "John Doe" });`), + code: createActorDispatcherCode( + `return new Person({ name: "John Doe" });`, + ), rule, ruleName, federationSetup: ` @@ -333,7 +289,7 @@ export function createIdRequiredRuleTests(config: TestConfig): TestSuite { // ✅ Good - with id property (any value) "with id property any value": [ lintTest({ - code: createDispatcherCode(`return new Person({ + code: createActorDispatcherCode(`return new Person({ id: "https://example.com/users/123", name: "John Doe", });`), @@ -346,7 +302,7 @@ export function createIdRequiredRuleTests(config: TestConfig): TestSuite { // ✅ Good - with id property using ctx.getActorUri() "with id property using getActorUri": [ lintTest({ - code: createDispatcherCode(`return new Person({ + code: createActorDispatcherCode(`return new Person({ ${ID_PROP} name: "John Doe", });`), @@ -359,7 +315,7 @@ export function createIdRequiredRuleTests(config: TestConfig): TestSuite { // ✅ Good - BlockStatement with id "block statement with id": [ lintTest({ - code: createDispatcherCode(`const name = "John Doe"; + code: createActorDispatcherCode(`const name = "John Doe"; return new Person({ ${ID_PROP} name, @@ -373,7 +329,9 @@ export function createIdRequiredRuleTests(config: TestConfig): TestSuite { // ❌ Bad - without id property "without id property": [ lintTest({ - code: createDispatcherCode(`return new Person({ name: "John Doe" });`), + code: createActorDispatcherCode( + `return new Person({ name: "John Doe" });`, + ), rule, ruleName, expectedError, @@ -384,7 +342,7 @@ export function createIdRequiredRuleTests(config: TestConfig): TestSuite { // ❌ Bad - returning empty object "returning empty object": [ lintTest({ - code: createDispatcherCode(`return new Person({});`), + code: createActorDispatcherCode(`return new Person({});`), rule, ruleName, expectedError, @@ -395,7 +353,7 @@ export function createIdRequiredRuleTests(config: TestConfig): TestSuite { // ✅ Good - multiple properties including id "multiple properties including id": [ lintTest({ - code: createDispatcherCode(`return new Person({ + code: createActorDispatcherCode(`return new Person({ ${ID_PROP} name: "John Doe", inbox: ctx.getInboxUri(identifier), @@ -410,7 +368,7 @@ export function createIdRequiredRuleTests(config: TestConfig): TestSuite { // ❌ Bad - variable assignment without id "variable assignment without id": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `const actor = new Person({ name: "John Doe" }); return actor;`, ), @@ -445,6 +403,9 @@ export function createMismatchRuleTests( .filter((p) => p.getter !== prop.getter) .map((p) => p.getter); const wrongGetter = wrongGetters[0] || "getWrongUri"; + const wrongSetter = Object.values(properties) + .filter((p) => p.setter !== prop.setter) + .map((p) => p.setter)[0] || "setWrongDispatcher"; const createLocalPropertyCode = (getter: string) => createPropertyAssignment(prop, { getter }); @@ -460,7 +421,7 @@ export function createMismatchRuleTests( // ✅ Good - non-Federation object "non-federation object": [ lintTest({ - code: createDispatcherCode(createActorCode(wrongGetter)), + code: createDispatcherCode(createActorCode(wrongGetter), wrongSetter), rule, ruleName, federationSetup: ` @@ -473,7 +434,7 @@ export function createMismatchRuleTests( // ✅ Good - correct getter used "correct getter used": [ lintTest({ - code: createDispatcherCode(createActorCode(prop.getter)), + code: createDispatcherCode(createActorCode(prop.getter), prop.setter), rule, ruleName, }), @@ -483,7 +444,7 @@ export function createMismatchRuleTests( // ❌ Bad - wrong getter used "wrong getter used": [ lintTest({ - code: createDispatcherCode(createActorCode(wrongGetter)), + code: createDispatcherCode(createActorCode(wrongGetter), wrongSetter), rule, ruleName, expectedError, @@ -492,9 +453,9 @@ export function createMismatchRuleTests( ], // ❌ Bad - wrong identifier - "wrong identifier": [ + "wrong context": [ lintTest({ - code: createDispatcherCode(`return new Person({ + code: createActorDispatcherCode(`return new Person({ ${ID_PROP} ${createPropertyAssignment(prop, { ctxName: "wrongContext" })} name: "John Doe", @@ -509,7 +470,7 @@ export function createMismatchRuleTests( // ✅ Good - property not present (no error) "property not present": [ lintTest({ - code: createDispatcherCode(`return new Person({ + code: createActorDispatcherCode(`return new Person({ ${ID_PROP} name: "John Doe", });`), @@ -534,7 +495,7 @@ export function createIdMismatchRuleTests(config: TestConfig): TestSuite { // ✅ Good - non-Federation object "non-federation object": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `return new Person({ id: ctx.getFollowingUri(identifier) });`, ), rule, @@ -549,8 +510,8 @@ export function createIdMismatchRuleTests(config: TestConfig): TestSuite { // ✅ Good - correct getter used "correct getter used": [ lintTest({ - code: createDispatcherCode( - `return new Person({ id: ctx.getActorUri(identifier) });`, + code: createActorDispatcherCode( + `return new Person({ ${ID_PROP} });`, ), rule, ruleName, @@ -561,7 +522,7 @@ export function createIdMismatchRuleTests(config: TestConfig): TestSuite { // ❌ Bad - literal string id "literal string id": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `return new Person({ id: "https://example.com/users/123" });`, ), rule, @@ -574,7 +535,7 @@ export function createIdMismatchRuleTests(config: TestConfig): TestSuite { // ❌ Bad - new URL as id "new URL as id": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `return new Person({ id: new URL("https://example.com/users/123") });`, ), rule, @@ -587,7 +548,7 @@ export function createIdMismatchRuleTests(config: TestConfig): TestSuite { // ❌ Bad - wrong getter used "wrong getter used": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `return new Person({ id: ctx.getFollowingUri(identifier) });`, ), rule, @@ -598,9 +559,9 @@ export function createIdMismatchRuleTests(config: TestConfig): TestSuite { ], // ❌ Bad - wrong identifier - "wrong identifier": [ + "wrong context": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `return new Person({ id: wrongContext.getActorUri(identifier) });`, ), rule, @@ -619,184 +580,191 @@ export function createIdMismatchRuleTests(config: TestConfig): TestSuite { /** * Creates common edge case tests for required rules */ -export function createRequiredEdgeCaseTests( - propertyKey: PropertyKey, - config: TestConfig, -): TestSuite { - const { rule, ruleName } = config; - const prop = properties[propertyKey]; - const setter = prop.setter; - const expectedError = actorPropertyRequired(prop); - - const createLocalPropertyCode = () => createPropertyAssignment(prop); - const propCode = createLocalPropertyCode(); +export const createRequiredEdgeCaseTests = requiredEdgeCaseTestsFactory( + createDispatcherCode, +); - return { - // ✅ Ternary with property in both branches - "ternary with property in both branches": [ - lintTest({ - code: createChainedDispatcherCode( - `\ - return condition - ? new Person({ ${ID_PROP} \ - ${propCode} name: "A" }) +/** + * Creates edge case tests for key required rules (publicKey, assertionMethod) + */ +export const createKeyRequiredEdgeCaseTests = requiredEdgeCaseTestsFactory( + createChainedDispatcherCode, +); + +function requiredEdgeCaseTestsFactory( + createDispatcherCode: (content: string, dispatcherMethod: string) => string, +) { + return function ( + propertyKey: PropertyKey, + config: TestConfig, + ): TestSuite { + const { rule, ruleName } = config; + const prop = properties[propertyKey]; + const setter = prop.setter; + const expectedError = actorPropertyRequired(prop); + const propCode = createPropertyAssignment(prop); + + return { + // ✅ Ternary with property in both branches + "ternary with property in both branches": [ + lintTest({ + code: createDispatcherCode( + `return condition + ? new Person({ ${ID_PROP} ${propCode} name: "A" }) : new Person({ ${ID_PROP} ${propCode} name: "B" });`, - setter, - ), - rule, - ruleName, - }), - true, - ], - - // ❌ Ternary missing property in consequent - "ternary missing property in consequent": [ - lintTest({ - code: createChainedDispatcherCode( - `\ - return condition + setter, + ), + rule, + ruleName, + }), + true, + ], + + // ❌ Ternary missing property in consequent + "ternary missing property in consequent": [ + lintTest({ + code: createDispatcherCode( + `return condition ? new Person({ ${ID_PROP} name: "A" }) : new Person({ ${ID_PROP} ${propCode} name: "B" });`, - setter, - ), - rule, - ruleName, - expectedError, - }), - false, - ], - - // ❌ Ternary missing property in alternate - "ternary missing property in alternate": [ - lintTest({ - code: createChainedDispatcherCode( - `\ - return condition + setter, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ❌ Ternary missing property in alternate + "ternary missing property in alternate": [ + lintTest({ + code: createDispatcherCode( + `return condition ? new Person({ ${ID_PROP} ${propCode} name: "A" }) : new Person({ ${ID_PROP} name: "B" });`, - setter, - ), - rule, - ruleName, - expectedError, - }), - false, - ], - - // ❌ Ternary missing property in both - "ternary missing property in both branches": [ - lintTest({ - code: createChainedDispatcherCode( - `return condition + setter, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ❌ Ternary missing property in both + "ternary missing property in both branches": [ + lintTest({ + code: createDispatcherCode( + `return condition ? new Person({ ${ID_PROP} name: "A" }) : new Person({ ${ID_PROP} name: "B" });`, - setter, - ), - rule, - ruleName, - expectedError, - }), - false, - ], - - // ✅ Nested ternary with property - "nested ternary with property": [ - lintTest({ - code: createChainedDispatcherCode( - `\ -return condition1 + setter, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ✅ Nested ternary with property + "nested ternary with property": [ + lintTest({ + code: createDispatcherCode( + `return condition1 ? (condition2 ? new Person({ ${ID_PROP} ${propCode} name: "A" }) : new Person({ ${ID_PROP} ${propCode} name: "B" })) : new Person({ ${ID_PROP} ${propCode} name: "C" });`, - setter, - ), - rule, - ruleName, - }), - true, - ], - - // ✅ If/else with property in both branches - "if else with property in both branches": [ - lintTest({ - code: createChainedDispatcherCode( - `\ + setter, + ), + rule, + ruleName, + }), + true, + ], + + // ✅ If/else with property in both branches + "if else with property in both branches": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition) { return new Person({ ${ID_PROP} ${propCode} name: "A" }); } else { return new Person({ ${ID_PROP} ${propCode} name: "B" }); }`, - setter, - ), - rule, - ruleName, - }), - true, - ], - - // ❌ If/else missing property in if block - "if else missing property in if block": [ - lintTest({ - code: createChainedDispatcherCode( - `\ + setter, + ), + rule, + ruleName, + }), + true, + ], + + // ❌ If/else missing property in if block + "if else missing property in if block": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition) { return new Person({ ${ID_PROP} name: "A" }); } else { return new Person({ ${ID_PROP} ${propCode} name: "B" }); }`, - setter, - ), - rule, - ruleName, - expectedError, - }), - false, - ], - - // ❌ If/else missing property in else block - "if else missing property in else block": [ - lintTest({ - code: createChainedDispatcherCode( - `\ + setter, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ❌ If/else missing property in else block + "if else missing property in else block": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition) { return new Person({ ${ID_PROP} ${propCode} name: "A" }); } else { return new Person({ ${ID_PROP} name: "B" }); }`, - setter, - ), - rule, - ruleName, - expectedError, - }), - false, - ], - - // ❌ If/else missing property in both blocks - "if else missing property in both blocks": [ - lintTest({ - code: createChainedDispatcherCode( - `\ + setter, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ❌ If/else missing property in both blocks + "if else missing property in both blocks": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition) { return new Person({ ${ID_PROP} name: "A" }); } else { return new Person({ ${ID_PROP} name: "B" }); }`, - setter, - ), - rule, - ruleName, - expectedError, - }), - false, - ], - - // ✅ Nested if with property - "nested if with property": [ - lintTest({ - code: createChainedDispatcherCode( - `\ + setter, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ✅ Nested if with property + "nested if with property": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition1) { if (condition2) { return new Person({ ${ID_PROP} ${propCode} name: "A" }); @@ -806,19 +774,19 @@ if (condition1) { } else { return new Person({ ${ID_PROP} ${propCode} name: "C" }); }`, - setter, - ), - rule, - ruleName, - }), - true, - ], - - // ✅ If/else if/else with property in all branches - "if else if else with property in all branches": [ - lintTest({ - code: createChainedDispatcherCode( - `\ + setter, + ), + rule, + ruleName, + }), + true, + ], + + // ✅ If/else if/else with property in all branches + "if else if else with property in all branches": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition1) { return new Person({ ${ID_PROP} ${propCode} name: "A" }); } else if (condition2) { @@ -826,19 +794,19 @@ if (condition1) { } else { return new Person({ ${ID_PROP} ${propCode} name: "C" }); }`, - setter, - ), - rule, - ruleName, - }), - true, - ], - - // ❌ If/else if/else missing property in else if - "if else if else missing property in else if": [ - lintTest({ - code: createChainedDispatcherCode( - `\ + setter, + ), + rule, + ruleName, + }), + true, + ], + + // ❌ If/else if/else missing property in else if + "if else if else missing property in else if": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition1) { return new Person({ ${ID_PROP} ${propCode} name: "A" }); } else if (condition2) { @@ -846,53 +814,54 @@ if (condition1) { } else { return new Person({ ${ID_PROP} ${propCode} name: "C" }); }`, - setter, - ), - rule, - ruleName, - expectedError, - }), - false, - ], - - // ✅ If/else if with final return, property in all paths - "if else if with final return property in all paths": [ - lintTest({ - code: createChainedDispatcherCode( - `\ + setter, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ✅ If/else if with final return, property in all paths + "if else if with final return property in all paths": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition1) { return new Person({ ${ID_PROP} ${propCode} name: "A" }); } else if (condition2) { return new Person({ ${ID_PROP} ${propCode} name: "B" }); } return new Person({ ${ID_PROP} ${propCode} name: "C" });`, - setter, - ), - rule, - ruleName, - }), - true, - ], - - // ❌ If/else if with final return, missing property in final return - "if else if with final return missing property in final return": [ - lintTest({ - code: createChainedDispatcherCode( - `\ + setter, + ), + rule, + ruleName, + }), + true, + ], + + // ❌ If/else if with final return, missing property in final return + "if else if with final return missing property in final return": [ + lintTest({ + code: createDispatcherCode( + `\ if (condition1) { return new Person({ ${ID_PROP} ${propCode} name: "A" }); } else if (condition2) { return new Person({ ${ID_PROP} ${propCode} name: "B" }); } return new Person({ ${ID_PROP} name: "C" });`, - setter, - ), - rule, - ruleName, - expectedError, - }), - false, - ], + setter, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + }; }; } @@ -923,7 +892,7 @@ export function createMismatchEdgeCaseTests( // ✅ Ternary with correct getter in both branches "ternary with correct getter in both branches": [ lintTest({ - code: createDispatcherCode(nestedTernaryCode(propCode, propCode)), + code: createActorDispatcherCode(nestedTernaryCode(propCode, propCode)), rule, ruleName, }), @@ -933,7 +902,9 @@ export function createMismatchEdgeCaseTests( // ❌ Ternary with wrong getter in consequent "ternary with wrong getter in consequent": [ lintTest({ - code: createDispatcherCode(nestedTernaryCode(wrongPropCode, propCode)), + code: createActorDispatcherCode( + nestedTernaryCode(wrongPropCode, propCode), + ), rule, ruleName, expectedError, @@ -944,7 +915,9 @@ export function createMismatchEdgeCaseTests( // ❌ Ternary with wrong getter in alternate "ternary with wrong getter in alternate": [ lintTest({ - code: createDispatcherCode(nestedTernaryCode(propCode, wrongPropCode)), + code: createActorDispatcherCode( + nestedTernaryCode(propCode, wrongPropCode), + ), rule, ruleName, expectedError, @@ -955,7 +928,7 @@ export function createMismatchEdgeCaseTests( // ❌ Ternary with wrong getter in both "ternary with wrong getter in both branches": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( nestedTernaryCode(wrongPropCode, wrongPropCode), ), rule, @@ -968,7 +941,7 @@ export function createMismatchEdgeCaseTests( // ✅ Nested ternary with correct getter "nested ternary with correct getter": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `\ return condition1 ? (condition2 @@ -985,7 +958,7 @@ return condition1 // ✅ If/else with correct getter in both branches "if else with correct getter in both branches": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `\ if (condition) { return new Person({ ${ID_PROP} ${propCode} name: "A" }); @@ -1002,7 +975,7 @@ if (condition) { // ❌ If/else with wrong getter in if block "if else with wrong getter in if block": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `\ if (condition) { return new Person({ ${ID_PROP} ${wrongPropCode} name: "A" }); @@ -1020,7 +993,7 @@ if (condition) { // ❌ If/else with wrong getter in else block "if else with wrong getter in else block": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `\ if (condition) { return new Person({ ${ID_PROP} ${propCode} name: "A" }); @@ -1038,7 +1011,7 @@ if (condition) { // ❌ If/else with wrong getter in both blocks "if else with wrong getter in both blocks": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `\ if (condition) { return new Person({ ${ID_PROP} ${wrongPropCode} name: "A" }); @@ -1056,7 +1029,7 @@ if (condition) { // ✅ Nested if with correct getter "nested if with correct getter": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `\ if (condition1) { if (condition2) { @@ -1077,7 +1050,7 @@ if (condition1) { // ✅ If/else if/else with correct getter in all branches "if else if else with correct getter in all branches": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `\ if (condition1) { return new Person({ ${ID_PROP} ${propCode} name: "A" }); @@ -1096,7 +1069,7 @@ if (condition1) { // ❌ If/else if/else with wrong getter in else if "if else if else with wrong getter in else if": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `\ if (condition1) { return new Person({ ${ID_PROP} ${propCode} name: "A" }); @@ -1116,7 +1089,7 @@ if (condition1) { // ✅ If/else if with final return, correct getter in all paths "if else if with final return correct getter in all paths": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `\ if (condition1) { return new Person({ ${ID_PROP} ${propCode} name: "A" }); @@ -1134,7 +1107,7 @@ return new Person({ ${ID_PROP} ${propCode} name: "C" });`, // ❌ If/else if with final return, wrong getter in final return "if else if with final return wrong getter in final return": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `\ if (condition1) { return new Person({ ${ID_PROP} ${propCode} name: "A" }); @@ -1168,7 +1141,7 @@ export function createIdRequiredEdgeCaseTests(config: TestConfig): TestSuite { // ✅ Ternary with id in both branches "ternary with id in both branches": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `return condition ? new Person({ ${ID_PROP} name: "A" }) : new Person({ ${ID_PROP} name: "B" });`, @@ -1182,7 +1155,7 @@ export function createIdRequiredEdgeCaseTests(config: TestConfig): TestSuite { // ❌ Ternary missing id in consequent "ternary missing id in consequent": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `return condition ? new Person({ name: "A" }) : new Person({ ${ID_PROP} name: "B" });`, @@ -1197,7 +1170,7 @@ export function createIdRequiredEdgeCaseTests(config: TestConfig): TestSuite { // ❌ Ternary missing id in alternate "ternary missing id in alternate": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `return condition ? new Person({ ${ID_PROP} name: "A" }) : new Person({ name: "B" });`, @@ -1212,7 +1185,7 @@ export function createIdRequiredEdgeCaseTests(config: TestConfig): TestSuite { // ❌ Ternary missing id in both "ternary missing id in both branches": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `return condition ? new Person({ name: "A" }) : new Person({ name: "B" });`, @@ -1227,7 +1200,7 @@ export function createIdRequiredEdgeCaseTests(config: TestConfig): TestSuite { // ✅ Nested ternary with id "nested ternary with id": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `return condition1 ? (condition2 ? new Person({ ${ID_PROP} name: "A" }) @@ -1243,7 +1216,7 @@ export function createIdRequiredEdgeCaseTests(config: TestConfig): TestSuite { // ✅ If/else with id in both branches "if else with id in both branches": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `\ if (condition) { return new Person({ ${ID_PROP} name: "A" }); @@ -1260,7 +1233,7 @@ if (condition) { // ❌ If/else missing id in if block "if else missing id in if block": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `\ if (condition) { return new Person({ name: "A" }); @@ -1278,7 +1251,7 @@ if (condition) { // ❌ If/else missing id in else block "if else missing id in else block": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `\ if (condition) { return new Person({ ${ID_PROP} name: "A" }); @@ -1296,7 +1269,7 @@ if (condition) { // ❌ If/else missing id in both blocks "if else missing id in both blocks": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `\ if (condition) { return new Person({ name: "A" }); @@ -1314,7 +1287,7 @@ if (condition) { // ✅ Nested if with id "nested if with id": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `\ if (condition1) { if (condition2) { @@ -1335,7 +1308,7 @@ if (condition1) { // ✅ If/else if/else with id in all branches "if else if else with id in all branches": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `\ if (condition1) { return new Person({ ${ID_PROP} name: "A" }); @@ -1354,7 +1327,7 @@ if (condition1) { // ❌ If/else if/else missing id in else if "if else if else missing id in else if": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `\ if (condition1) { return new Person({ ${ID_PROP} name: "A" }); @@ -1374,7 +1347,7 @@ if (condition1) { // ✅ If/else if with final return, id in all paths "if else if with final return id in all paths": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `\ if (condition1) { return new Person({ ${ID_PROP} name: "A" }); @@ -1392,7 +1365,7 @@ return new Person({ ${ID_PROP} name: "C" });`, // ❌ If/else if with final return, missing id in final return "if else if with final return missing id in final return": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `\ if (condition1) { return new Person({ ${ID_PROP} name: "A" }); @@ -1423,7 +1396,7 @@ export function createIdMismatchEdgeCaseTests(config: TestConfig): TestSuite { // ✅ Ternary with correct getter in both branches "ternary with correct getter in both branches": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `return condition ? new Person({ ${ID_PROP} name: "A" }) : new Person({ ${ID_PROP} name: "B" });`, @@ -1437,7 +1410,7 @@ export function createIdMismatchEdgeCaseTests(config: TestConfig): TestSuite { // ❌ Ternary with wrong getter in consequent "ternary with wrong getter in consequent": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `return condition ? new Person({ id: ctx.getFollowingUri(identifier), name: "A" }) : new Person({ ${ID_PROP} name: "B" });`, @@ -1452,7 +1425,7 @@ export function createIdMismatchEdgeCaseTests(config: TestConfig): TestSuite { // ❌ Ternary with wrong getter in alternate "ternary with wrong getter in alternate": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `return condition ? new Person({ ${ID_PROP} name: "A" }) : new Person({ id: ctx.getFollowingUri(identifier), name: "B" });`, @@ -1467,7 +1440,7 @@ export function createIdMismatchEdgeCaseTests(config: TestConfig): TestSuite { // ❌ Ternary with wrong getter in both "ternary with wrong getter in both branches": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `return condition ? new Person({ id: ctx.getFollowingUri(identifier), name: "A" }) : new Person({ id: ctx.getFollowingUri(identifier), name: "B" });`, @@ -1482,7 +1455,7 @@ export function createIdMismatchEdgeCaseTests(config: TestConfig): TestSuite { // ✅ Nested ternary with correct getter "nested ternary with correct getter": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `return condition1 ? (condition2 ? new Person({ ${ID_PROP} name: "A" }) @@ -1498,7 +1471,7 @@ export function createIdMismatchEdgeCaseTests(config: TestConfig): TestSuite { // ✅ If/else with correct getter in both branches "if else with correct getter in both branches": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `\ if (condition) { return new Person({ ${ID_PROP} name: "A" }); @@ -1515,7 +1488,7 @@ if (condition) { // ❌ If/else with wrong getter in if block "if else with wrong getter in if block": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `\ if (condition) { return new Person({ id: ctx.getFollowingUri(identifier), name: "A" }); @@ -1533,7 +1506,7 @@ if (condition) { // ❌ If/else with wrong getter in else block "if else with wrong getter in else block": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `\ if (condition) { return new Person({ ${ID_PROP} name: "A" }); @@ -1551,7 +1524,7 @@ if (condition) { // ❌ If/else with wrong getter in both blocks "if else with wrong getter in both blocks": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `\ if (condition) { return new Person({ id: ctx.getFollowingUri(identifier), name: "A" }); @@ -1569,7 +1542,7 @@ if (condition) { // ✅ Nested if with correct getter "nested if with correct getter": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `\ if (condition1) { if (condition2) { @@ -1590,7 +1563,7 @@ if (condition1) { // ✅ If/else if/else with correct getter in all branches "if else if else with correct getter in all branches": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `\ if (condition1) { return new Person({ ${ID_PROP} name: "A" }); @@ -1609,7 +1582,7 @@ if (condition1) { // ❌ If/else if/else with wrong getter in else if "if else if else with wrong getter in else if": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `\ if (condition1) { return new Person({ ${ID_PROP} name: "A" }); @@ -1629,7 +1602,7 @@ if (condition1) { // ✅ If/else if with final return, correct getter in all paths "if else if with final return correct getter in all paths": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `\ if (condition1) { return new Person({ ${ID_PROP} name: "A" }); @@ -1647,7 +1620,7 @@ return new Person({ ${ID_PROP} name: "C" });`, // ❌ If/else if with final return, wrong getter in final return "if else if with final return wrong getter in final return": [ lintTest({ - code: createDispatcherCode( + code: createActorDispatcherCode( `\ if (condition1) { return new Person({ ${ID_PROP} name: "A" }); @@ -1664,279 +1637,3 @@ return new Person({ id: ctx.getFollowingUri(identifier), name: "C" });`, ], }; } - -/** - * Creates edge case tests for key required rules (publicKey, assertionMethod) - */ -export function _createRequiredEdgeCaseTests( - propertyName: "publicKey" | "assertionMethod", - config: TestConfig, -): TestSuite { - const { rule, ruleName } = config; - const prop = properties[propertyName]; - const expectedError = actorPropertyRequired(prop); - - const createPropertyCode = () => - `${propertyName}: ctx.getActorKeyPairs(identifier),`; - const propCode = createPropertyCode(); - - return { - // ✅ Ternary with property in both branches - "ternary with property in both branches": [ - lintTest({ - code: createChainedDispatcherCode( - nestedTernaryCode(propCode, propCode), - "setKeyPairsDispatcher", - ), - rule, - ruleName, - }), - true, - ], - - // ❌ Ternary missing property in consequent - "ternary missing property in consequent": [ - lintTest({ - code: createChainedDispatcherCode( - `\ - return condition - ? new Person({ ${ID_PROP} name: "A" }) - : new Person({ ${ID_PROP} ${propCode} name: "B" });`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - expectedError, - }), - false, - ], - - // ❌ Ternary missing property in alternate - "ternary missing property in alternate": [ - lintTest({ - code: createChainedDispatcherCode( - `\ - return condition - ? new Person({ ${ID_PROP} ${propCode} name: "A" }) - : new Person({ ${ID_PROP} name: "B" });`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - expectedError, - }), - false, - ], - - // ❌ Ternary missing property in both - "ternary missing property in both branches": [ - lintTest({ - code: createChainedDispatcherCode( - `return condition - ? new Person({ ${ID_PROP} name: "A" }) - : new Person({ ${ID_PROP} name: "B" });`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - expectedError, - }), - false, - ], - - // ✅ Nested ternary with property - "nested ternary with property": [ - lintTest({ - code: createChainedDispatcherCode( - `\ -return condition1 - ? (condition2 - ? new Person({ ${ID_PROP} ${propCode} name: "A" }) - : new Person({ ${ID_PROP} ${propCode} name: "B" })) - : new Person({ ${ID_PROP} ${propCode} name: "C" });`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - }), - true, - ], - - // ✅ If/else with property in both branches - "if else with property in both branches": [ - lintTest({ - code: createChainedDispatcherCode( - `\ -if (condition) { - return new Person({ ${ID_PROP} ${propCode} name: "A" }); -} else { - return new Person({ ${ID_PROP} ${propCode} name: "B" }); -}`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - }), - true, - ], - - // ❌ If/else missing property in if block - "if else missing property in if block": [ - lintTest({ - code: createChainedDispatcherCode( - `\ -if (condition) { - return new Person({ ${ID_PROP} name: "A" }); -} else { - return new Person({ ${ID_PROP} ${propCode} name: "B" }); -}`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - expectedError, - }), - false, - ], - - // ❌ If/else missing property in else block - "if else missing property in else block": [ - lintTest({ - code: createChainedDispatcherCode( - `\ -if (condition) { - return new Person({ ${ID_PROP} ${propCode} name: "A" }); -} else { - return new Person({ ${ID_PROP} name: "B" }); -}`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - expectedError, - }), - false, - ], - - // ❌ If/else missing property in both blocks - "if else missing property in both blocks": [ - lintTest({ - code: createChainedDispatcherCode( - `\ -if (condition) { - return new Person({ ${ID_PROP} name: "A" }); -} else { - return new Person({ ${ID_PROP} name: "B" }); -}`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - expectedError, - }), - false, - ], - - // ✅ Nested if with property - "nested if with property": [ - lintTest({ - code: createChainedDispatcherCode( - `\ -if (condition1) { - if (condition2) { - return new Person({ ${ID_PROP} ${propCode} name: "A" }); - } else { - return new Person({ ${ID_PROP} ${propCode} name: "B" }); - } -} else { - return new Person({ ${ID_PROP} ${propCode} name: "C" }); -}`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - }), - true, - ], - - // ✅ If/else if/else with property in all branches - "if else if else with property in all branches": [ - lintTest({ - code: createChainedDispatcherCode( - `\ -if (condition1) { - return new Person({ ${ID_PROP} ${propCode} name: "A" }); -} else if (condition2) { - return new Person({ ${ID_PROP} ${propCode} name: "B" }); -} else { - return new Person({ ${ID_PROP} ${propCode} name: "C" }); -}`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - }), - true, - ], - - // ❌ If/else if/else missing property in else if - "if else if else missing property in else if": [ - lintTest({ - code: createChainedDispatcherCode( - `\ -if (condition1) { - return new Person({ ${ID_PROP} ${propCode} name: "A" }); -} else if (condition2) { - return new Person({ ${ID_PROP} name: "B" }); -} else { - return new Person({ ${ID_PROP} ${propCode} name: "C" }); -}`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - expectedError, - }), - false, - ], - - // ✅ If/else if with final return, property in all paths - "if else if with final return property in all paths": [ - lintTest({ - code: createChainedDispatcherCode( - `\ -if (condition1) { - return new Person({ ${ID_PROP} ${propCode} name: "A" }); -} else if (condition2) { - return new Person({ ${ID_PROP} ${propCode} name: "B" }); -} -return new Person({ ${ID_PROP} ${propCode} name: "C" });`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - }), - true, - ], - - // ❌ If/else if with final return, missing property in final return - "if else if with final return missing property in final return": [ - lintTest({ - code: createChainedDispatcherCode( - `\ -if (condition1) { - return new Person({ ${ID_PROP} ${propCode} name: "A" }); -} else if (condition2) { - return new Person({ ${ID_PROP} ${propCode} name: "B" }); -} -return new Person({ ${ID_PROP} name: "C" });`, - "setKeyPairsDispatcher", - ), - rule, - ruleName, - expectedError, - }), - false, - ], - }; -} diff --git a/packages/lint/src/tests/actor-assertion-method-required.test.ts b/packages/lint/src/tests/actor-assertion-method-required.test.ts index d0bb75e44..6bc163b88 100644 --- a/packages/lint/src/tests/actor-assertion-method-required.test.ts +++ b/packages/lint/src/tests/actor-assertion-method-required.test.ts @@ -1,7 +1,7 @@ import { RULE_IDS } from "../lib/const.ts"; import { - createRequiredDispatcherRuleTests, - createRequiredEdgeCaseTests, + createKeyRequiredDispatcherRuleTests, + createKeyRequiredEdgeCaseTests, runTests, } from "../lib/test-templates.ts"; import * as rule from "../rules/actor-assertion-method-required.ts"; @@ -11,6 +11,6 @@ const config = { rule, ruleName }; runTests( ruleName, - createRequiredDispatcherRuleTests("assertionMethod", config), + createKeyRequiredDispatcherRuleTests("assertionMethod", config), ); -runTests(ruleName, createRequiredEdgeCaseTests("assertionMethod", config)); +runTests(ruleName, createKeyRequiredEdgeCaseTests("assertionMethod", config)); diff --git a/packages/lint/src/tests/actor-public-key-required.test.ts b/packages/lint/src/tests/actor-public-key-required.test.ts index fabe92402..be3b51261 100644 --- a/packages/lint/src/tests/actor-public-key-required.test.ts +++ b/packages/lint/src/tests/actor-public-key-required.test.ts @@ -1,7 +1,7 @@ import { RULE_IDS } from "../lib/const.ts"; import { - createRequiredDispatcherRuleTests, - createRequiredEdgeCaseTests, + createKeyRequiredDispatcherRuleTests, + createKeyRequiredEdgeCaseTests, runTests, } from "../lib/test-templates.ts"; import * as rule from "../rules/actor-public-key-required.ts"; @@ -9,5 +9,5 @@ import * as rule from "../rules/actor-public-key-required.ts"; const ruleName = RULE_IDS.actorPublicKeyRequired; const config = { rule, ruleName }; -runTests(ruleName, createRequiredDispatcherRuleTests("publicKey", config)); -runTests(ruleName, createRequiredEdgeCaseTests("publicKey", config)); +runTests(ruleName, createKeyRequiredDispatcherRuleTests("publicKey", config)); +runTests(ruleName, createKeyRequiredEdgeCaseTests("publicKey", config)); From 0fa2d83a5f7a1114d5cf647df87f6e96ce023662 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Fri, 19 Dec 2025 11:34:20 +0000 Subject: [PATCH 38/41] Separated key required test --- packages/lint/src/lib/test-templates.ts | 149 ++++++++++++++---------- 1 file changed, 88 insertions(+), 61 deletions(-) diff --git a/packages/lint/src/lib/test-templates.ts b/packages/lint/src/lib/test-templates.ts index 8f1dca65a..bd18454ff 100644 --- a/packages/lint/src/lib/test-templates.ts +++ b/packages/lint/src/lib/test-templates.ts @@ -160,13 +160,39 @@ const createTestCode = ( * (following, followers, outbox, liked, featured, featuredTags) */ export const createRequiredDispatcherRuleTests = - requiredDispatcherRuleTestsFactory(createDispatcherCode); + requiredDispatcherRuleTestsFactory(createDispatcherCode, false); export const createKeyRequiredDispatcherRuleTests = - requiredDispatcherRuleTestsFactory(createChainedDispatcherCode); + requiredDispatcherRuleTestsFactory(createChainedDispatcherCode, true); +function requiredDispatcherRuleTestsFactory( + createDispatcherCode: ( + content: string, + dispatcherMethod: string, + isBefore?: boolean, + ) => string, + isKeyRequired: false, +): ( + propertyKey: PropertyKey, + config: TestConfig, +) => TestSuite; function requiredDispatcherRuleTestsFactory( createDispatcherCode: (content: string, dispatcherMethod: string) => string, -) { + isKeyRequired: true, +): ( + propertyKey: PropertyKey, + config: TestConfig, +) => TestSuite; +function requiredDispatcherRuleTestsFactory( + createDispatcherCode: ( + content: string, + dispatcherMethod: string, + isBefore?: boolean, + ) => string, + isKeyRequired: boolean, +): ( + propertyKey: PropertyKey, + config: TestConfig, +) => TestSuite { return function ( propertyKey: PropertyKey, config: TestConfig, @@ -201,64 +227,65 @@ function requiredDispatcherRuleTestsFactory( }), true, ], - - // ✅ Good - dispatcher configured BEFORE - "dispatcher before separate with property": [ - lintTest({ - code: createDispatcherCode( - createTestCode(propertyKey, true), - prop.setter, - true, - ), - rule, - ruleName, - }), - true, - ], - - // ✅ Good - dispatcher configured AFTER - "dispatcher after separate with property": [ - lintTest({ - code: createDispatcherCode( - createTestCode(propertyKey, true), - prop.setter, - false, - ), - rule, - ruleName, - }), - true, - ], - - // ❌ Bad - dispatcher before, property missing - "dispatcher before separate property missing": [ - lintTest({ - code: createDispatcherCode( - createTestCode(propertyKey, false), - prop.setter, - true, - ), - rule, - ruleName, - expectedError, - }), - false, - ], - - // ❌ Bad - dispatcher after, property missing - "dispatcher after separate property missing": [ - lintTest({ - code: createDispatcherCode( - createTestCode(propertyKey, false), - prop.setter, - false, - ), - rule, - ruleName, - expectedError, - }), - false, - ], + ...(isKeyRequired ? {} : { + // ✅ Good - dispatcher configured BEFORE + "dispatcher before separate with property": [ + lintTest({ + code: createDispatcherCode( + createTestCode(propertyKey, true), + prop.setter, + true, + ), + rule, + ruleName, + }), + true, + ], + + // ✅ Good - dispatcher configured AFTER + "dispatcher after separate with property": [ + lintTest({ + code: createDispatcherCode( + createTestCode(propertyKey, true), + prop.setter, + false, + ), + rule, + ruleName, + }), + true, + ], + + // ❌ Bad - dispatcher before, property missing + "dispatcher before separate property missing": [ + lintTest({ + code: createDispatcherCode( + createTestCode(propertyKey, false), + prop.setter, + true, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + + // ❌ Bad - dispatcher after, property missing + "dispatcher after separate property missing": [ + lintTest({ + code: createDispatcherCode( + createTestCode(propertyKey, false), + prop.setter, + false, + ), + rule, + ruleName, + expectedError, + }), + false, + ], + }), }; }; } From a652c2e77f36a0ab0f831a040895dce843a24f10 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Tue, 23 Dec 2025 11:44:29 +0000 Subject: [PATCH 39/41] Added docs about `@fedify/lint` --- docs/.vitepress/config.mts | 2 + docs/manual/lint.md | 1124 +++++++++++++++++++++++++++++ docs/package.json | 1 + docs/tutorial/basics.md | 91 +-- packages/lint/src/lib/messages.ts | 2 +- pnpm-lock.yaml | 25 +- 6 files changed, 1142 insertions(+), 103 deletions(-) create mode 100644 docs/manual/lint.md diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 8123fa48d..b4414692c 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -76,6 +76,7 @@ const MANUAL = { { text: "Message queue", link: "/manual/mq.md" }, { text: "Integration", link: "/manual/integration.md" }, { text: "Testing", link: "/manual/test.md" }, + { text: "Linting", link: "/manual/lint.md" }, { text: "Logging", link: "/manual/log.md" }, { text: "OpenTelemetry", link: "/manual/opentelemetry.md" }, { text: "Deployment", link: "/manual/deploy.md" }, @@ -100,6 +101,7 @@ const REFERENCES = { { text: "@fedify/sqlite", link: "https://jsr.io/@fedify/sqlite/doc" }, { text: "@fedify/sveltekit", link: "https://jsr.io/@fedify/sveltekit/doc" }, { text: "@fedify/testing", link: "https://jsr.io/@fedify/testing/doc" }, + { text: "@fedify/lint", link: "https://jsr.io/@fedify/lint/doc" }, ], }; diff --git a/docs/manual/lint.md b/docs/manual/lint.md new file mode 100644 index 000000000..8f94ade10 --- /dev/null +++ b/docs/manual/lint.md @@ -0,0 +1,1124 @@ +--- +description: >- + Fedify provides linting plugins for Deno Lint and ESLint to help you catch + common mistakes and enforce best practices when building federated server + apps. +--- + +# Linting + +_This package is available since Fedify 2.0.0._ + +> [!TIP] +> We highly recommend using the `@fedify/lint` package in your federated server +> app to catch common mistakes early and enforce best practices. + +Fedify provides the [`@fedify/lint`] package, which includes lint rules +specifically designed for Fedify applications. It supports both [Deno Lint] and +[ESLint], so you can use it regardless of your JavaScript/TypeScript runtime. + +The plugin includes rules that check for: + +- Proper actor ID configuration +- Required actor properties (inbox, outbox, followers, etc.) +- Correct URL patterns for actor collections +- Public key and assertion method requirements +- Collection filtering implementation + +[`@fedify/lint`]: https://jsr.io/@fedify/lint +[Deno Lint]: https://docs.deno.com/runtime/reference/lint_plugins/ +[ESLint]: https://eslint.org/ + +## Installation + +::: code-group + +```sh [Deno] +deno add jsr:@fedify/lint +``` + +```sh [npm] +npm add -D @fedify/lint +``` + +```sh [pnpm] +pnpm add -D @fedify/lint +``` + +```sh [Yarn] +yarn add -D @fedify/lint +``` + +```sh [Bun] +bun add -D @fedify/lint +``` + +::: + +## Deno Lint + +### Basic setup + +Add the plugin to your _deno.json_ configuration file: + +```json +{ + "lint": { + "plugins": ["jsr:@fedify/lint"] + } +} +``` + +By default, this enables all recommended rules. + +### Custom configuration + +You can customize which rules to enable and their severity levels: + +```json +{ + "lint": { + "plugins": ["jsr:@fedify/lint"], + "rules": { + "tags": ["recommended"], + "include": [ + "@fedify/lint/actor-id-required", + "@fedify/lint/actor-id-mismatch" + ], + "exclude": [ + "@fedify/lint/actor-featured-property-required" + ] + } + } +} +``` + +### Running Deno Lint + +After setting up the configuration, run Deno's linter: + +```sh +deno lint +``` + +You can also specify which files to lint: + +```sh +deno lint federation.ts +deno lint src/federation/ +``` + +## ESLint + +### Basic setup + +Add the plugin to your ESLint configuration file (e.g., _eslint.config.ts_ or +_eslint.config.js_): + +```typescript twoslash +import fedifyLint from "@fedify/lint"; + +// If your `createFederation` code is in `federation.ts` or `federation/**.ts` +export default fedifyLint; + +// Or specify your own federation files +export default { + ...fedifyLint, + files: ["my-own-federation.ts"], +}; + +// If you use other ESLint configurations +export default [ + // otherConfig, + fedifyLint, +]; +``` + +The default configuration applies recommended rules to files that match common +federation-related patterns (e.g., _federation.ts_, _federation/\*.ts_). + +### Custom configuration + +You can customize which files to lint and which rules to enable: + +```typescript twoslash +import { plugin } from "@fedify/lint"; + +export default [{ + files: ["src/federation/**/*.ts"], // Your federation code location + plugins: { + "@fedify/lint": plugin, + }, + rules: { + "@fedify/lint/actor-id-required": "error", + "@fedify/lint/actor-id-mismatch": "error", + "@fedify/lint/actor-inbox-property-required": "warn", + // ... other rules + }, +}]; +``` + +### Using configurations + +The plugin provides two preset configurations: + +#### Recommended (default) + +Enables critical rules as errors and optional rules as warnings: + +```typescript twoslash +import fedifyLint from "@fedify/lint"; + +export default fedifyLint; +``` + +#### Strict + +Enables all rules as errors: + +```typescript twoslash +import { plugin } from "@fedify/lint"; + +export default [{ + files: ["**/*.ts"], + ...plugin.configs.strict, +}]; +``` + +### Running ESLint + +Set up your ESLint configuration as shown above and add a script to +_package.json_: + +```jsonc +{ + "scripts": { + "lint": "eslint ." + } +} +``` + +After setting up the configuration, run ESLint on your codebase: + +::: code-group + +```sh [npm] +npm run lint +``` + +```sh [pnpm] +pnpm lint +``` + +```sh [Yarn] +yarn lint +``` + +```sh [Bun] +bun lint +``` + +::: + +Or run the linter directly via command line: + +::: code-group + +```sh [npm] +npx eslint . +``` + +```sh [pnpm] +pnpx eslint . +``` + +```sh [Yarn] +yarn eslint . +``` + +```sh [Bun] +bunx eslint . +``` + +::: + +## Rules + +### `actor-id-required` + +Ensures all actors have an `id` property in the actor dispatcher. + +**When this rule applies:** +The actor dispatcher returns a `Person`, `Organization`, `Group`, `Application`, +or `Service` object without an `id` property. + +**Why it matters:** +Every ActivityPub actor must have a unique identifier (ID) to be discoverable +and to receive activities from other servers. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Missing id property +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + name: "John Doe", // No id! + }); +}); + +// ✅ Good: Include id property +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + name: "John Doe", + }); +}); +~~~~ + +### `actor-id-mismatch` + +Validates that actor IDs match the expected URI from `Context.getActorUri()`. + +**When this rule applies:** +The `id` property is set to a value other than `ctx.getActorUri(identifier)`, +such as a hardcoded URL string, `new URL(...)`, or a different context method. + +**Why it matters:** +Using the wrong URI for the actor ID can cause federation issues. Other servers +won't be able to properly verify the actor's identity or send activities to it. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Using hardcoded URL +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: new URL(`https://example.com/users/${identifier}`), + name: "John Doe", + }); +}); + +// ❌ Bad: Using wrong context method +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getFollowersUri(identifier), // Wrong method! + name: "John Doe", + }); +}); + +// ✅ Good: Use ctx.getActorUri() +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + name: "John Doe", + }); +}); +~~~~ + +### `actor-public-key-required` + +Ensures actors have public keys for [HTTP Signatures]. + +**When this rule applies:** +The actor dispatcher is chained with `setKeyPairsDispatcher()`, but the actor +object doesn't include a `publicKey` or `publicKeys` property. + +**Why it matters:** +HTTP Signatures are used to verify the authenticity of activities. Without +a public key, other servers cannot verify that activities sent by your actor +are legitimate. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Missing publicKey when setKeyPairsDispatcher is configured +federation + .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + name: "John Doe", + // Missing publicKey! + }); + }) + .setKeyPairsDispatcher(async (ctx, identifier) => { + // Returns key pairs... + return []; + }); + +// ✅ Good: Include publicKey from key pairs dispatcher +federation + .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + const keyPairs = await ctx.getActorKeyPairs(identifier); + return new Person({ + id: ctx.getActorUri(identifier), + name: "John Doe", + publicKey: keyPairs[0].cryptographicKey, + }); + }) + .setKeyPairsDispatcher(async (ctx, identifier) => { + // Returns key pairs... + return []; + }); +~~~~ + +[HTTP Signatures]: ./send.md#http-signatures + +### `actor-assertion-method-required` + +Validates that actors have assertion methods for [Object Integrity Proofs]. + +**When this rule applies:** +The actor dispatcher is chained with `setKeyPairsDispatcher()`, but the actor +object doesn't include an `assertionMethod` property. + +**Why it matters:** +Object Integrity Proofs use assertion methods to cryptographically sign +activities. This provides an additional layer of security beyond HTTP +Signatures. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Missing assertionMethod when setKeyPairsDispatcher is configured +federation + .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + const keyPairs = await ctx.getActorKeyPairs(identifier); + return new Person({ + id: ctx.getActorUri(identifier), + name: "John Doe", + publicKey: keyPairs[0].cryptographicKey, + // Missing assertionMethod! + }); + }) + .setKeyPairsDispatcher(async (ctx, identifier) => { + // Returns key pairs... + return []; + }); + +// ✅ Good: Include assertionMethod from key pairs dispatcher +federation + .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + const keyPairs = await ctx.getActorKeyPairs(identifier); + return new Person({ + id: ctx.getActorUri(identifier), + name: "John Doe", + publicKey: keyPairs[0].cryptographicKey, + assertionMethod: keyPairs[0].multikey, + }); + }) + .setKeyPairsDispatcher(async (ctx, identifier) => { + // Returns key pairs... + return []; + }); +~~~~ + +[Object Integrity Proofs]: ./send.md#object-integrity-proofs + +### `actor-inbox-property-required` + +Ensures `inbox` is defined when `setInboxListeners()` is configured. + +**When this rule applies:** +You've called `federation.setInboxListeners()` to handle incoming activities, +but the actor object doesn't include an `inbox` property. + +**Why it matters:** +The inbox URL tells other servers where to send activities to your actor. +Without it, your actor cannot receive follow requests, mentions, or any other +activities. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Missing inbox when setInboxListeners is configured +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + name: "John Doe", + // Missing inbox! + }); +}); + +federation.setInboxListeners("/users/{identifier}/inbox", "/inbox"); + +// ✅ Good: Include inbox property +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + name: "John Doe", + inbox: ctx.getInboxUri(identifier), + }); +}); + +federation.setInboxListeners("/users/{identifier}/inbox", "/inbox"); +~~~~ + +### `actor-inbox-property-mismatch` + +Validates that the `inbox` URI is set using `ctx.getInboxUri(identifier)`. + +**When this rule applies:** +The `inbox` property is set to a value other than `ctx.getInboxUri(identifier)`. + +**Why it matters:** +The inbox URI must match the path configured in `setInboxListeners()`. Using +a different URI will cause incoming activities to fail. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Using hardcoded URL +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + inbox: new URL(`https://example.com/inbox/${identifier}`), // Wrong! + }); +}); + +// ✅ Good: Use ctx.getInboxUri() +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + inbox: ctx.getInboxUri(identifier), + }); +}); +~~~~ + +### `actor-outbox-property-required` + +Ensures `outbox` is defined when `setOutboxDispatcher()` is configured. + +**When this rule applies:** +You've called `federation.setOutboxDispatcher()` to serve the actor's outbox, +but the actor object doesn't include an `outbox` property. + +**Why it matters:** +The outbox URL allows other servers and users to view the actor's published +activities. It's part of the standard ActivityPub actor profile. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Missing outbox when setOutboxDispatcher is configured +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + // Missing outbox! + }); +}); + +federation.setOutboxDispatcher( + "/users/{identifier}/outbox", + (ctx, identifier) => ({ items: [] }) +); + +// ✅ Good: Include outbox property +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + outbox: ctx.getOutboxUri(identifier), + }); +}); +~~~~ + +### `actor-outbox-property-mismatch` + +Validates that the `outbox` URI is set using `ctx.getOutboxUri(identifier)`. + +**When this rule applies:** +The `outbox` property is set to a value other than +`ctx.getOutboxUri(identifier)`. + +**Why it matters:** +The outbox URI must match the path configured in `setOutboxDispatcher()`. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Using wrong context method +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + outbox: ctx.getInboxUri(identifier), // Wrong method! + }); +}); + +// ✅ Good: Use ctx.getOutboxUri() +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + outbox: ctx.getOutboxUri(identifier), + }); +}); +~~~~ + +### `actor-followers-property-required` + +Ensures `followers` is defined when `setFollowersDispatcher()` is configured. + +**When this rule applies:** +You've called `federation.setFollowersDispatcher()` to serve the actor's +followers collection, but the actor object doesn't include a `followers` +property. + +**Why it matters:** +The followers URL allows other servers to discover who follows this actor, +which is important for activity delivery and social graph discovery. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Missing followers when setFollowersDispatcher is configured +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + // Missing followers! + }); +}); + +federation.setFollowersDispatcher( + "/users/{identifier}/followers", + (ctx, identifier) => ({ items: [] }) +); + +// ✅ Good: Include followers property +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + followers: ctx.getFollowersUri(identifier), + }); +}); +~~~~ + +### `actor-followers-property-mismatch` + +Validates that the `followers` URI is set using +`ctx.getFollowersUri(identifier)`. + +**When this rule applies:** +The `followers` property is set to a value other than +`ctx.getFollowersUri(identifier)`. + +**Why it matters:** +The followers URI must match the path configured in `setFollowersDispatcher()`. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Using wrong context method +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + followers: ctx.getFollowingUri(identifier), // Wrong method! + }); +}); + +// ✅ Good: Use ctx.getFollowersUri() +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + followers: ctx.getFollowersUri(identifier), + }); +}); +~~~~ + +### `actor-following-property-required` + +Ensures `following` is defined when `setFollowingDispatcher()` is configured. + +**When this rule applies:** +You've called `federation.setFollowingDispatcher()` to serve the actor's +following collection, but the actor object doesn't include a `following` +property. + +**Why it matters:** +The following URL allows other servers to discover who this actor follows. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Missing following when setFollowingDispatcher is configured +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + // Missing following! + }); +}); + +federation.setFollowingDispatcher( + "/users/{identifier}/following", + (ctx, identifier) => ({ items: [] }) +); + +// ✅ Good: Include following property +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + following: ctx.getFollowingUri(identifier), + }); +}); +~~~~ + +### `actor-following-property-mismatch` + +Validates that the `following` URI is set using +`ctx.getFollowingUri(identifier)`. + +**When this rule applies:** +The `following` property is set to a value other than +`ctx.getFollowingUri(identifier)`. + +**Why it matters:** +The following URI must match the path configured in `setFollowingDispatcher()`. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Using wrong context method +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + following: ctx.getFollowersUri(identifier), // Wrong method! + }); +}); + +// ✅ Good: Use ctx.getFollowingUri() +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + following: ctx.getFollowingUri(identifier), + }); +}); +~~~~ + +### `actor-liked-property-required` + +Ensures `liked` is defined when `setLikedDispatcher()` is configured. + +**When this rule applies:** +You've called `federation.setLikedDispatcher()` to serve the actor's liked +collection, but the actor object doesn't include a `liked` property. + +**Why it matters:** +The liked URL allows other servers to discover what content this actor has +liked. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Missing liked when setLikedDispatcher is configured +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + // Missing liked! + }); +}); + +federation.setLikedDispatcher( + "/users/{identifier}/liked", + (ctx, identifier) => ({ items: [] }) +); + +// ✅ Good: Include liked property +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + liked: ctx.getLikedUri(identifier), + }); +}); +~~~~ + +### `actor-liked-property-mismatch` + +Validates that the `liked` URI is set using `ctx.getLikedUri(identifier)`. + +**When this rule applies:** +The `liked` property is set to a value other than `ctx.getLikedUri(identifier)`. + +**Why it matters:** +The liked URI must match the path configured in `setLikedDispatcher()`. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Using wrong context method +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + liked: ctx.getFollowersUri(identifier), // Wrong method! + }); +}); + +// ✅ Good: Use ctx.getLikedUri() +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + liked: ctx.getLikedUri(identifier), + }); +}); +~~~~ + +### `actor-featured-property-required` + +Ensures `featured` is defined when `setFeaturedDispatcher()` is configured. + +**When this rule applies:** +You've called `federation.setFeaturedDispatcher()` to serve the actor's +featured/pinned posts collection, but the actor object doesn't include a +`featured` property. + +**Why it matters:** +The featured URL allows other servers to discover the actor's pinned or +highlighted content (commonly shown at the top of a profile). + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Missing featured when setFeaturedDispatcher is configured +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + // Missing featured! + }); +}); + +federation.setFeaturedDispatcher( + "/users/{identifier}/featured", + (ctx, identifier) => ({ items: [] }) +); + +// ✅ Good: Include featured property +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + featured: ctx.getFeaturedUri(identifier), + }); +}); +~~~~ + +### `actor-featured-property-mismatch` + +Validates that the `featured` URI is set using +`ctx.getFeaturedUri(identifier)`. + +**When this rule applies:** +The `featured` property is set to a value other than +`ctx.getFeaturedUri(identifier)`. + +**Why it matters:** +The featured URI must match the path configured in `setFeaturedDispatcher()`. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Using wrong context method +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + featured: ctx.getFollowersUri(identifier), // Wrong method! + }); +}); + +// ✅ Good: Use ctx.getFeaturedUri() +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + featured: ctx.getFeaturedUri(identifier), + }); +}); +~~~~ + +### `actor-featured-tags-property-required` + +Ensures `featuredTags` is defined when `setFeaturedTagsDispatcher()` is +configured. + +**When this rule applies:** +You've called `federation.setFeaturedTagsDispatcher()` to serve the actor's +featured hashtags collection, but the actor object doesn't include a +`featuredTags` property. + +**Why it matters:** +The featuredTags URL allows other servers to discover the actor's featured +hashtags (commonly used for profile discovery). + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Missing featuredTags when setFeaturedTagsDispatcher is configured +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + // Missing featuredTags! + }); +}); + +federation.setFeaturedTagsDispatcher( + "/users/{identifier}/tags", + (ctx, identifier) => ({ items: [] }) +); + +// ✅ Good: Include featuredTags property +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + featuredTags: ctx.getFeaturedTagsUri(identifier), + }); +}); +~~~~ + +### `actor-featured-tags-property-mismatch` + +Validates that the `featuredTags` URI is set using +`ctx.getFeaturedTagsUri(identifier)`. + +**When this rule applies:** +The `featuredTags` property is set to a value other than +`ctx.getFeaturedTagsUri(identifier)`. + +**Why it matters:** +The featuredTags URI must match the path configured in +`setFeaturedTagsDispatcher()`. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Using wrong context method +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + featuredTags: ctx.getFollowersUri(identifier), // Wrong method! + }); +}); + +// ✅ Good: Use ctx.getFeaturedTagsUri() +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + featuredTags: ctx.getFeaturedTagsUri(identifier), + }); +}); +~~~~ + +### `actor-shared-inbox-property-required` + +Ensures `endpoints.sharedInbox` is defined when `setInboxListeners()` is +configured with a shared inbox path. + +**When this rule applies:** +You've called `federation.setInboxListeners()` with a second parameter (shared +inbox path), but the actor object doesn't include an +`endpoints: new Endpoints({ sharedInbox: ... })` property. + +**Why it matters:** +The shared inbox allows other servers to send activities to multiple actors +on your server with a single request, improving federation efficiency. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Endpoints, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Missing sharedInbox when setInboxListeners has shared inbox path +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + inbox: ctx.getInboxUri(identifier), + // Missing endpoints.sharedInbox! + }); +}); + +federation.setInboxListeners("/users/{identifier}/inbox", "/inbox"); + +// ✅ Good: Include endpoints.sharedInbox +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + inbox: ctx.getInboxUri(identifier), + endpoints: new Endpoints({ + sharedInbox: ctx.getInboxUri(), + }), + }); +}); +~~~~ + +### `actor-shared-inbox-property-mismatch` + +Validates that `endpoints.sharedInbox` is set using `ctx.getInboxUri()` (without +identifier). + +**When this rule applies:** +The `endpoints.sharedInbox` property is set to a value other than +`ctx.getInboxUri()` (called without arguments for the shared inbox). + +**Why it matters:** +The shared inbox URI must match the shared inbox path configured in +`setInboxListeners()`. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Endpoints, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Using getInboxUri with identifier for shared inbox +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + inbox: ctx.getInboxUri(identifier), + endpoints: new Endpoints({ + sharedInbox: ctx.getInboxUri(identifier), // Wrong! Should be no args + }), + }); +}); + +// ✅ Good: Use ctx.getInboxUri() without arguments +federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), + inbox: ctx.getInboxUri(identifier), + endpoints: new Endpoints({ + sharedInbox: ctx.getInboxUri(), // No identifier for shared inbox + }), + }); +}); +~~~~ + +### `collection-filtering-not-implemented` + +Warns when collection dispatchers don't implement filtering. + +**When this rule applies:** +The `setFollowersDispatcher()` callback function has fewer than 4 parameters +(missing the `filter` parameter). + +> [!NOTE] +> Currently, this rule only checks `setFollowersDispatcher()`. Other collection +> dispatchers may be added in the future. + +**Why it matters:** +Collection filtering allows clients to request specific subsets of a collection, +reducing response payload sizes and improving performance. Without filtering, +large collections could cause performance issues. + +For more information, see the [*Filtering by server*] section in the +collections manual. + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Bad: Missing filter parameter +federation.setFollowersDispatcher( + "/users/{identifier}/followers", + async (ctx, identifier, cursor) => { // Only 3 parameters! + return { items: [] }; + } +); + +// ✅ Good: Include filter parameter (4th parameter) +federation.setFollowersDispatcher( + "/users/{identifier}/followers", + async (ctx, identifier, cursor, filter) => { + // Use filter to handle filtering requests + return { items: [] }; + } +); +~~~~ + +[*Filtering by server*]: ./collections.md#filtering-by-server + +## Example + +Here's an example of code that would trigger lint errors: + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ❌ Wrong: Using relative URL for actor ID +federation.setActorDispatcher( + "/{identifier}", + (_ctx, identifier) => { + return new Person({ + id: new URL(`/${identifier}`), // ❌ Should use ctx.getActorUri() + name: "Example User", + }); + }, +); +~~~~ + +Corrected version: + +~~~~ typescript twoslash +// @noErrors: 2345 +import { createFederation, Person } from "@fedify/fedify"; +const federation = createFederation({ kv: null as any }); +// ---cut-before--- +// ✅ Correct: Using Context.getActorUri() for actor ID +federation.setActorDispatcher( + "/{identifier}", + (ctx, identifier) => { + return new Person({ + id: ctx.getActorUri(identifier), // ✅ Correct + name: "Example User", + inbox: ctx.getInboxUri(identifier), + outbox: ctx.getOutboxUri(identifier), + followers: ctx.getFollowersUri(identifier), + // ... other required properties + }); + }, +); +~~~~ + +When you run the linter on the incorrect code, you'll see an error like: + +``` +error[fedify-lint/actor-id-mismatch]: Actor's `id` property must match +`ctx.getActorUri(identifier)`. Ensure you're using the correct context method. +``` + +## See also + +- [`@fedify/lint` on JSR](https://jsr.io/@fedify/lint) +- [`@fedify/lint` on npm](https://www.npmjs.com/package/@fedify/lint) +- [Deno Lint plugins documentation](https://docs.deno.com/runtime/reference/lint_plugins/) +- [ESLint documentation](https://eslint.org/) +- [Example project](https://github.com/fedify-dev/fedify/tree/main/examples/lint) diff --git a/docs/package.json b/docs/package.json index e1e8ad353..8498c23cf 100644 --- a/docs/package.json +++ b/docs/package.json @@ -11,6 +11,7 @@ "@fedify/h3": "workspace:", "@fedify/hono": "workspace:", "@fedify/koa": "workspace:", + "@fedify/lint": "workspace:", "@fedify/nestjs": "workspace:", "@fedify/next": "workspace:", "@fedify/postgres": "workspace:", diff --git a/docs/tutorial/basics.md b/docs/tutorial/basics.md index e6f532576..ced599ee8 100644 --- a/docs/tutorial/basics.md +++ b/docs/tutorial/basics.md @@ -1283,9 +1283,9 @@ Linting your federation code ----------------------------- As your federated server grows, it's important to maintain code quality and -catch common mistakes early. Fedify provides linting plugins for both -[Deno Lint] and [ESLint], which include specialized linting rules for -federation code. +catch common mistakes early. Fedify provides the [`@fedify/lint`] package +with linting plugins for both Deno Lint and ESLint, which include specialized +linting rules for federation code. The `@fedify/lint` package helps you avoid common pitfalls such as: @@ -1295,79 +1295,6 @@ The `@fedify/lint` package helps you avoid common pitfalls such as: - Incorrect URL patterns for actor collections - Missing public keys or assertion methods -### Using Deno Lint - -If you are using Deno, you can use the Deno Lint plugin. First, install the -plugin: - -~~~~ sh -deno add jsr:@fedify/lint -~~~~ - -Then add the plugin to your *deno.json* configuration file: - -~~~~ json -{ - "lint": { - "plugins": ["jsr:@fedify/lint"] - } -} -~~~~ - -Now you can run the linter on your code: - -~~~~ sh -deno lint -~~~~ - -### Using ESLint - -For ESLint users (including Deno, Bun, and Node.js), first install the plugin: - -::: code-group - -~~~~ sh [Bun] -bun add -D @fedify/lint eslint -~~~~ - -~~~~ sh [Node.js] -npm add -D @fedify/lint eslint -~~~~ - -::: - -Then create an ESLint configuration file (e.g., *eslint.config.ts*) in your -project directory: - -~~~~ typescript twoslash -import fedifyLint from "@fedify/lint"; - -export default [{ - ...fedifyLint, - files: ["**/*.ts"], -}]; -~~~~ - -Now you can run the linter on your code: - -::: code-group - -~~~~ sh [Bun] -bun run eslint . -~~~~ - -~~~~ sh [Node.js] -npx eslint . -~~~~ - -::: - -> [!NOTE] -> When using ESLint with Deno, you may need to exclude some Deno-specific -> lint rules like `no-unused-vars` to avoid conflicts. - -### Example - For example, if you had written the actor dispatcher like this: ~~~~ typescript @@ -1390,15 +1317,13 @@ error[fedify-lint/actor-id-mismatch]: Actor's `id` property must match `_ctx.getActorUri(identifier)`. Ensure you're using the correct context method. ~~~~ -This helps you catch mistakes before they cause issues in production. The -linter includes many other rules to ensure your federation code follows best -practices. +This helps you catch mistakes before they cause issues in production. -For more information about available rules and configuration options, see -the [`@fedify/lint` documentation](https://jsr.io/@fedify/lint). +> [!TIP] +> For detailed setup instructions, available rules, and configuration options, +> see the [*Linting* section](../manual/lint.md) in the manual. -[Deno Lint]: https://docs.deno.com/runtime/reference/lint_plugins/ -[ESLint]: https://eslint.org/ +[`@fedify/lint`]: https://jsr.io/@fedify/lint Wrapping up diff --git a/packages/lint/src/lib/messages.ts b/packages/lint/src/lib/messages.ts index e15f0f779..af160860d 100644 --- a/packages/lint/src/lib/messages.ts +++ b/packages/lint/src/lib/messages.ts @@ -22,7 +22,7 @@ export const actorPropertyRequired = ({ path: path.join("."), requiresIdentifier, }) - }\`for the \`${path.join(".")}\` property URI.`; + }\` for the \`${path.join(".")}\` property URI.`; /** * Generates error message for *-mismatch rules. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 83c80c60a..246666400 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -147,6 +147,9 @@ importers: '@fedify/koa': specifier: 'workspace:' version: link:../packages/koa + '@fedify/lint': + specifier: 'workspace:' + version: link:../packages/lint '@fedify/nestjs': specifier: 'workspace:' version: link:../packages/nestjs @@ -275,7 +278,7 @@ importers: version: 1.6.3(patch_hash=56bd737eca4c1ba581d00bedd4fe307f6e48f782af59933eac54bb7d43206b99)(@algolia/client-search@5.29.0)(@types/node@22.19.1)(@types/react@18.3.23)(lightningcss@1.30.1)(postcss@8.5.6)(search-insights@2.17.3)(typescript@5.9.3) vitepress-plugin-group-icons: specifier: ^1.3.5 - version: 1.6.1(markdown-it@14.1.0)(vite@7.1.3(@types/node@22.19.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.1)) + version: 1.6.1(markdown-it@14.1.0)(vite@5.4.19(@types/node@22.19.1)(lightningcss@1.30.1)) vitepress-plugin-llms: specifier: ^1.1.0 version: 1.6.0 @@ -16587,22 +16590,6 @@ snapshots: fsevents: 2.3.3 lightningcss: 1.30.1 - vite@7.1.3(@types/node@22.19.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.1): - dependencies: - esbuild: 0.25.5 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.44.1 - tinyglobby: 0.2.14 - optionalDependencies: - '@types/node': 22.19.1 - fsevents: 2.3.3 - jiti: 2.5.1 - lightningcss: 1.30.1 - tsx: 4.20.3 - yaml: 2.8.1 - vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.1): dependencies: esbuild: 0.25.5 @@ -16623,13 +16610,13 @@ snapshots: optionalDependencies: vite: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.1) - vitepress-plugin-group-icons@1.6.1(markdown-it@14.1.0)(vite@7.1.3(@types/node@22.19.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.1)): + vitepress-plugin-group-icons@1.6.1(markdown-it@14.1.0)(vite@5.4.19(@types/node@22.19.1)(lightningcss@1.30.1)): dependencies: '@iconify-json/logos': 1.2.4 '@iconify-json/vscode-icons': 1.2.23 '@iconify/utils': 2.3.0 markdown-it: 14.1.0 - vite: 7.1.3(@types/node@22.19.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.1) + vite: 5.4.19(@types/node@22.19.1)(lightningcss@1.30.1) transitivePeerDependencies: - supports-color From 8436c8609d18888d615e9a7e63d19685c2cb8e01 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Tue, 23 Dec 2025 12:03:11 +0000 Subject: [PATCH 40/41] Fixed error --- docs/manual/lint.md | 108 ++++++++++++++++++++++++-------------------- 1 file changed, 60 insertions(+), 48 deletions(-) diff --git a/docs/manual/lint.md b/docs/manual/lint.md index 8f94ade10..36dff3e61 100644 --- a/docs/manual/lint.md +++ b/docs/manual/lint.md @@ -33,25 +33,25 @@ The plugin includes rules that check for: ::: code-group -```sh [Deno] +~~~~sh [Deno] deno add jsr:@fedify/lint -``` +~~~~ -```sh [npm] +~~~~sh [npm] npm add -D @fedify/lint -``` +~~~~ -```sh [pnpm] +~~~~sh [pnpm] pnpm add -D @fedify/lint -``` +~~~~ -```sh [Yarn] +~~~~sh [Yarn] yarn add -D @fedify/lint -``` +~~~~ -```sh [Bun] +~~~~sh [Bun] bun add -D @fedify/lint -``` +~~~~ ::: @@ -61,13 +61,13 @@ bun add -D @fedify/lint Add the plugin to your _deno.json_ configuration file: -```json +~~~~json { "lint": { "plugins": ["jsr:@fedify/lint"] } } -``` +~~~~ By default, this enables all recommended rules. @@ -75,7 +75,7 @@ By default, this enables all recommended rules. You can customize which rules to enable and their severity levels: -```json +~~~~json { "lint": { "plugins": ["jsr:@fedify/lint"], @@ -91,22 +91,22 @@ You can customize which rules to enable and their severity levels: } } } -``` +~~~~ ### Running Deno Lint After setting up the configuration, run Deno's linter: -```sh +~~~~sh deno lint -``` +~~~~ You can also specify which files to lint: -```sh +~~~~sh deno lint federation.ts deno lint src/federation/ -``` +~~~~ ## ESLint @@ -115,24 +115,36 @@ deno lint src/federation/ Add the plugin to your ESLint configuration file (e.g., _eslint.config.ts_ or _eslint.config.js_): -```typescript twoslash +~~~~ typescript twoslash import fedifyLint from "@fedify/lint"; // If your `createFederation` code is in `federation.ts` or `federation/**.ts` export default fedifyLint; +~~~~ + +Or specify your own federation files: -// Or specify your own federation files +~~~~ typescript twoslash +// @errors: 2304 +import fedifyLint from "@fedify/lint"; +// ---cut-before--- export default { ...fedifyLint, files: ["my-own-federation.ts"], }; +~~~~ + +If you use other ESLint configurations: -// If you use other ESLint configurations +~~~~ typescript twoslash +// @errors: 2304 +import fedifyLint from "@fedify/lint"; +// ---cut-before--- export default [ // otherConfig, fedifyLint, ]; -``` +~~~~ The default configuration applies recommended rules to files that match common federation-related patterns (e.g., _federation.ts_, _federation/\*.ts_). @@ -141,7 +153,7 @@ federation-related patterns (e.g., _federation.ts_, _federation/\*.ts_). You can customize which files to lint and which rules to enable: -```typescript twoslash +~~~~ typescript twoslash import { plugin } from "@fedify/lint"; export default [{ @@ -156,7 +168,7 @@ export default [{ // ... other rules }, }]; -``` +~~~~ ### Using configurations @@ -166,57 +178,57 @@ The plugin provides two preset configurations: Enables critical rules as errors and optional rules as warnings: -```typescript twoslash +~~~~ typescript twoslash import fedifyLint from "@fedify/lint"; export default fedifyLint; -``` +~~~~ #### Strict Enables all rules as errors: -```typescript twoslash +~~~~ typescript twoslash import { plugin } from "@fedify/lint"; export default [{ files: ["**/*.ts"], ...plugin.configs.strict, }]; -``` +~~~~ ### Running ESLint Set up your ESLint configuration as shown above and add a script to _package.json_: -```jsonc +~~~~jsonc { "scripts": { "lint": "eslint ." } } -``` +~~~~ After setting up the configuration, run ESLint on your codebase: ::: code-group -```sh [npm] +~~~~sh [npm] npm run lint -``` +~~~~ -```sh [pnpm] +~~~~sh [pnpm] pnpm lint -``` +~~~~ -```sh [Yarn] +~~~~sh [Yarn] yarn lint -``` +~~~~ -```sh [Bun] +~~~~sh [Bun] bun lint -``` +~~~~ ::: @@ -224,21 +236,21 @@ Or run the linter directly via command line: ::: code-group -```sh [npm] +~~~~sh [npm] npx eslint . -``` +~~~~ -```sh [pnpm] +~~~~sh [pnpm] pnpx eslint . -``` +~~~~ -```sh [Yarn] +~~~~sh [Yarn] yarn eslint . -``` +~~~~ -```sh [Bun] +~~~~sh [Bun] bunx eslint . -``` +~~~~ ::: @@ -1110,10 +1122,10 @@ federation.setActorDispatcher( When you run the linter on the incorrect code, you'll see an error like: -``` +~~~~ error[fedify-lint/actor-id-mismatch]: Actor's `id` property must match `ctx.getActorUri(identifier)`. Ensure you're using the correct context method. -``` +~~~~ ## See also From 5e4aef2f5378924dadae924c98267966c9e9943e Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Tue, 23 Dec 2025 12:12:28 +0000 Subject: [PATCH 41/41] Fixed style --- docs/manual/lint.md | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/manual/lint.md b/docs/manual/lint.md index 36dff3e61..569cb1637 100644 --- a/docs/manual/lint.md +++ b/docs/manual/lint.md @@ -5,7 +5,8 @@ description: >- apps. --- -# Linting +Linting +======= _This package is available since Fedify 2.0.0._ @@ -29,7 +30,8 @@ The plugin includes rules that check for: [Deno Lint]: https://docs.deno.com/runtime/reference/lint_plugins/ [ESLint]: https://eslint.org/ -## Installation +Installation +------------ ::: code-group @@ -55,7 +57,8 @@ bun add -D @fedify/lint ::: -## Deno Lint +Deno Lint +--------- ### Basic setup @@ -108,7 +111,8 @@ deno lint federation.ts deno lint src/federation/ ~~~~ -## ESLint +ESLint +------ ### Basic setup @@ -254,7 +258,8 @@ bunx eslint . ::: -## Rules +Rules +----- ### `actor-id-required` @@ -1076,7 +1081,8 @@ federation.setFollowersDispatcher( [*Filtering by server*]: ./collections.md#filtering-by-server -## Example +Example +------- Here's an example of code that would trigger lint errors: @@ -1127,7 +1133,8 @@ error[fedify-lint/actor-id-mismatch]: Actor's `id` property must match `ctx.getActorUri(identifier)`. Ensure you're using the correct context method. ~~~~ -## See also +See also +-------- - [`@fedify/lint` on JSR](https://jsr.io/@fedify/lint) - [`@fedify/lint` on npm](https://www.npmjs.com/package/@fedify/lint)