History

Enforce WorkOS Vault style rules with oxlint (#481)

Add seven repo-local oxlint rules scoped to packages/plugins/workos-vault/src to keep the provider's Effect-only surface, tagged-error discipline, and typed boundary patterns in place. Rules added: - no-inline-object-type-assertion - no-instanceof-tagged-error - no-manual-tag-check - no-promise-client-surface - no-raw-error-throw - no-redundant-error-factory - no-unknown-shape-probing Adapted to the existing flat scripts/oxlint-plugin-executor/utils.js layout on main; no rules already on main were duplicated. Cleanup in workos-vault to satisfy the new rules: - WorkOSVaultClientError now captures status + message at construction so callers consume typed fields instead of probing unknown causes. - Renamed exported WorkOSVaultSdk to module-private WorkOSVaultPromiseApi and removed it from the public package surface. - Replaced unwrapVaultError + inline shape probes in secret-store with direct typed access on WorkOSVaultClientError. - Introduced a named row type in secret-store.test.ts instead of an inline object type assertion.

Rhys Sullivan committed 6 months ago

Commit e3ee27f1 parent c5d93a314 files · +399 −60Browse files

Unified · Split

14 files
  1. .oxlintrc.jsonc +14 −0
  2. packages/plugins/workos-vault/src/sdk/client.ts +54 −19
  3. packages/plugins/workos-vault/src/sdk/index.ts +0 −1
  4. packages/plugins/workos-vault/src/sdk/secret-store.test.ts +11 −5
  5. packages/plugins/workos-vault/src/sdk/secret-store.ts +7 −35
  6. scripts/oxlint-plugin-executor.js +14 −0
  7. scripts/oxlint-plugin-executor/rules/no-inline-object-type-assertion.js +40 −0
  8. scripts/oxlint-plugin-executor/rules/no-instanceof-tagged-error.js +27 −0
  9. scripts/oxlint-plugin-executor/rules/no-manual-tag-check.js +25 −0
  10. scripts/oxlint-plugin-executor/rules/no-promise-client-surface.js +42 −0
  11. scripts/oxlint-plugin-executor/rules/no-raw-error-throw.js +24 −0
  12. scripts/oxlint-plugin-executor/rules/no-redundant-error-factory.js +52 −0
  13. scripts/oxlint-plugin-executor/rules/no-unknown-shape-probing.js +32 −0
  14. scripts/oxlint-plugin-executor/utils.js +57 −0
.oxlintrc.jsonc+14−0
@@ -24,5 +24,19 @@ · 23 unmodified lines
24 },24 },
25 ],25 ],
26 },26 },
27 "overrides": [
28 {
29 "files": ["packages/plugins/workos-vault/src/**/*.{ts,tsx}"],
30 "rules": {
31 "executor/no-inline-object-type-assertion": "error",
32 "executor/no-instanceof-tagged-error": "error",
33 "executor/no-manual-tag-check": "error",
34 "executor/no-promise-client-surface": "error",
35 "executor/no-raw-error-throw": "error",
36 "executor/no-redundant-error-factory": "error",
37 "executor/no-unknown-shape-probing": "error",
38 },
39 },
40 ],
27 "ignorePatterns": [".astro/", "**/routeTree.gen.ts", ".references/"],41 "ignorePatterns": [".astro/", "**/routeTree.gen.ts", ".references/"],
28}42}
packages/plugins/workos-vault/src/sdk/client.ts+54−19
@@ -1,5 +1,9 @@
1import type { WorkOS } from "@workos-inc/node/worker";1import type { WorkOS } from "@workos-inc/node/worker";
2import { WorkOS as WorkOSClient } from "@workos-inc/node/worker";2import {
3 GenericServerException,
4 NotFoundException,
5 WorkOS as WorkOSClient,
6} from "@workos-inc/node/worker";
3import { Data, Effect, Result } from "effect";7import { Data, Effect, Result } from "effect";
4 8
5export interface WorkOSVaultObjectMetadata {9export interface WorkOSVaultObjectMetadata {
@@ -16,10 +20,48 @@ export interface WorkOSVaultObject { · 10 unmodified lines
16 readonly value?: string;20 readonly value?: string;
17}21}
18 22
23// Minimal shape carrying an HTTP-style status code. Production WorkOS errors
24// (`GenericServerException`/`NotFoundException`) and test fakes both populate
25// a numeric `status`, so the boundary normalises against this named type
26// rather than probing arbitrary unknown shapes.
27interface ErrorWithStatus extends Error {
28 readonly status: number;
29}
30
31const isErrorWithStatus = (cause: unknown): cause is ErrorWithStatus =>
32 cause instanceof Error && typeof (cause as ErrorWithStatus).status === "number";
33
34const statusFromWorkOSCause = (cause: unknown): number | undefined => {
35 if (cause instanceof GenericServerException || cause instanceof NotFoundException) {
36 return cause.status;
37 }
38 if (isErrorWithStatus(cause)) return cause.status;
39 return undefined;
40};
41
42const messageFromWorkOSCause = (cause: unknown): string =>
43 cause instanceof Error ? cause.message : typeof cause === "string" ? cause : "";
44
19export class WorkOSVaultClientError extends Data.TaggedError("WorkOSVaultClientError")<{45export class WorkOSVaultClientError extends Data.TaggedError("WorkOSVaultClientError")<{
20 readonly cause: unknown;46 readonly cause: unknown;
47 readonly message: string;
21 readonly operation: string;48 readonly operation: string;
22}> {}49 readonly status?: number;
50}> {
51 constructor(options: {
52 readonly cause: unknown;
53 readonly message?: string;
54 readonly operation: string;
55 readonly status?: number;
56 }) {
57 super({
58 cause: options.cause,
59 message: options.message ?? messageFromWorkOSCause(options.cause),
60 operation: options.operation,
61 status: options.status ?? statusFromWorkOSCause(options.cause),
62 });
63 }
64}
23 65
24export class WorkOSVaultClientInstantiationError extends Data.TaggedError(66export class WorkOSVaultClientInstantiationError extends Data.TaggedError(
25 "WorkOSVaultClientInstantiationError",67 "WorkOSVaultClientInstantiationError",
@@ -27,7 +69,10 @@ export class WorkOSVaultClientInstantiationError extends Data.TaggedError( · 1 unmodified line
27 readonly cause: unknown;69 readonly cause: unknown;
28}> {}70}> {}
29 71
30export interface WorkOSVaultSdk {72// Promise-shaped facade onto the underlying WorkOS SDK. Module-private — the
73// public surface in `WorkOSVaultClient` is Effect-only. Test doubles import
74// this type to stand up an in-memory equivalent.
75export interface WorkOSVaultPromiseApi {
31 readonly createObject: (options: {76 readonly createObject: (options: {
32 readonly name: string;77 readonly name: string;
33 readonly value: string;78 readonly value: string;
@@ -55,7 +100,7 @@ interface WorkOSVaultUseOptions { · 21 unmodified lines
55export interface WorkOSVaultClient {100export interface WorkOSVaultClient {
56 readonly use: <A>(101 readonly use: <A>(
57 operation: string,102 operation: string,
58 fn: (client: WorkOSVaultSdk) => Promise<A>,103 fn: (client: WorkOSVaultPromiseApi) => Promise<A>,
59 options?: WorkOSVaultUseOptions,104 options?: WorkOSVaultUseOptions,
60 ) => Effect.Effect<A, WorkOSVaultClientError, never>;105 ) => Effect.Effect<A, WorkOSVaultClientError, never>;
61 readonly createObject: (options: {106 readonly createObject: (options: {
@@ -76,30 +121,20 @@ export interface WorkOSVaultClient { · 14 unmodified lines
76 }) => Effect.Effect<void, WorkOSVaultClientError, never>;121 }) => Effect.Effect<void, WorkOSVaultClientError, never>;
77}122}
78 123
79const vaultErrorStatus = (error: WorkOSVaultClientError): number | null => {
80 const cause = error.cause;
81 return typeof cause === "object" &&
82 cause !== null &&
83 "status" in cause &&
84 typeof (cause as { readonly status: unknown }).status === "number"
85 ? (cause as { readonly status: number }).status
86 : null;
87};
88
89const isExpectedVaultError = (124const isExpectedVaultError = (
90 error: WorkOSVaultClientError,125 error: WorkOSVaultClientError,
91 options: WorkOSVaultUseOptions | undefined,126 options: WorkOSVaultUseOptions | undefined,
92): boolean => {127): boolean => {
93 const status = vaultErrorStatus(error);128 if (error.status === undefined) return false;
94 return status !== null && (options?.expectedErrorStatuses?.includes(status) ?? false);129 return options?.expectedErrorStatuses?.includes(error.status) ?? false;
95};130};
96 131
97export const makeWorkOSVaultClient = (workos: Pick<WorkOS, "vault">): WorkOSVaultClient => {132export const makeWorkOSVaultClient = (workos: Pick<WorkOS, "vault">): WorkOSVaultClient => {
98 const client: WorkOSVaultSdk = workos.vault;133 const client: WorkOSVaultPromiseApi = workos.vault;
99 134
100 const use = <A>(135 const use = <A>(
101 operation: string,136 operation: string,
102 fn: (vault: WorkOSVaultSdk) => Promise<A>,137 fn: (vault: WorkOSVaultPromiseApi) => Promise<A>,
103 options?: WorkOSVaultUseOptions,138 options?: WorkOSVaultUseOptions,
104 ): Effect.Effect<A, WorkOSVaultClientError, never> => {139 ): Effect.Effect<A, WorkOSVaultClientError, never> => {
105 const attempt = Effect.tryPromise({140 const attempt = Effect.tryPromise({
@@ -116,7 +151,7 @@ export const makeWorkOSVaultClient = (workos: Pick<WorkOS, "vault">): WorkOSVaul · 10 unmodified lines
116 return outcome;151 return outcome;
117 }152 }
118 153
119 const status = vaultErrorStatus(outcome.failure);154 const status = outcome.failure.status;
120 if (isExpectedVaultError(outcome.failure, options)) {155 if (isExpectedVaultError(outcome.failure, options)) {
121 yield* Effect.annotateCurrentSpan({156 yield* Effect.annotateCurrentSpan({
122 "workos_vault.outcome": options?.expectedErrorOutcome ?? "expected_error",157 "workos_vault.outcome": options?.expectedErrorOutcome ?? "expected_error",
packages/plugins/workos-vault/src/sdk/index.ts+0−1
@@ -7,7 +7,6 @@ export { · 6 unmodified lines
7 type WorkOSVaultCredentials,7 type WorkOSVaultCredentials,
8 type WorkOSVaultObject,8 type WorkOSVaultObject,
9 type WorkOSVaultObjectMetadata,9 type WorkOSVaultObjectMetadata,
10 type WorkOSVaultSdk,
11} from "./client";10} from "./client";
12export {11export {
13 workosVaultPlugin,12 workosVaultPlugin,
packages/plugins/workos-vault/src/sdk/secret-store.test.ts+11−5
@@ -22,6 +22,14 @@ import { · 21 unmodified lines
22} from "./client";22} from "./client";
23import { workosVaultPlugin } from "./plugin";23import { workosVaultPlugin } from "./plugin";
24 24
25interface VaultMetadataRow {
26 readonly id: string;
27 readonly scope_id: string;
28 readonly name: string;
29 readonly purpose: string | null;
30 readonly created_at: Date;
31}
32
25// ---------------------------------------------------------------------------33// ---------------------------------------------------------------------------
26// Fake status errors — the real provider's isStatusError check pattern-34// Fake status errors — the real provider's isStatusError check pattern-
27// matches on a `status` field, so these bare Error subclasses are35// matches on a `status` field, so these bare Error subclasses are
@@ -465,13 +473,11 @@ describe("WorkOS Vault secret provider — multi-scope isolation", () => { · 437 unmodified lines
465 }),473 }),
466 );474 );
467 475
468 const rows = yield* adapter.findMany({476 const rows = (yield* adapter.findMany({
469 model: "workos_vault_metadata",477 model: "workos_vault_metadata",
470 where: [{ field: "id", value: "api-token" }],478 where: [{ field: "id", value: "api-token" }],
471 });479 })) as readonly VaultMetadataRow[];
472 const scopes = rows480 const scopes = rows.map((r) => r.scope_id).sort();
473 .map((r) => (r as { scope_id: string }).scope_id)
474 .sort();
475 expect(scopes).toEqual([outerId, innerId].sort());481 expect(scopes).toEqual([outerId, innerId].sort());
476 }),482 }),
477 );483 );
packages/plugins/workos-vault/src/sdk/secret-store.ts+7−35
@@ -1,5 +1,4 @@
1import { Effect } from "effect";1import { Effect } from "effect";
2import { GenericServerException, NotFoundException } from "@workos-inc/node/worker";
3 2
4import {3import {
5 defineSchema,4 defineSchema,
@@ -10,8 +9,8 @@ import { · 4 unmodified lines
10} from "@executor-js/sdk/core";9} from "@executor-js/sdk/core";
11 10
12import {11import {
13 WorkOSVaultClientError,
14 type WorkOSVaultClient,12 type WorkOSVaultClient,
13 type WorkOSVaultClientError,
15 type WorkOSVaultObject,14 type WorkOSVaultObject,
16} from "./client";15} from "./client";
17 16
@@ -155,35 +154,11 @@ export const makeWorkosVaultStore = ( · 137 unmodified lines
155// Vault helpers — scope-prefixed object naming + 409-retry upsert.154// Vault helpers — scope-prefixed object naming + 409-retry upsert.
156// ---------------------------------------------------------------------------155// ---------------------------------------------------------------------------
157 156
158const unwrapVaultError = (error: unknown): unknown =>157const isStatusError = (error: WorkOSVaultClientError, status: number): boolean =>
159 error instanceof WorkOSVaultClientError ? error.cause : error;158 error.status === status;
160
161const isStatusError = (error: unknown, status: number): boolean => {
162 const cause = unwrapVaultError(error);
163 return (
164 ((cause instanceof GenericServerException ||
165 cause instanceof NotFoundException) &&
166 cause.status === status) ||
167 (typeof cause === "object" &&
168 cause !== null &&
169 "status" in cause &&
170 typeof (cause as { status: unknown }).status === "number" &&
171 (cause as { status: number }).status === status)
172 );
173};
174 159
175const isKekNotReadyError = (error: unknown): boolean => {160const isKekNotReadyError = (error: WorkOSVaultClientError): boolean =>
176 const cause = unwrapVaultError(error);161 error.message.includes("KEK was created but is not yet ready");
177 const message =
178 cause instanceof Error
179 ? cause.message
180 : typeof cause === "string"
181 ? cause
182 : typeof cause === "object" && cause !== null && "message" in cause
183 ? String((cause as { message: unknown }).message)
184 : "";
185 return message.includes("KEK was created but is not yet ready");
186};
187 162
188// Default context builder. Each semantic piece of a scope id lives in163// Default context builder. Each semantic piece of a scope id lives in
189// its own vault-context key so WorkOS's KEK matcher sees individual164// its own vault-context key so WorkOS's KEK matcher sees individual
@@ -327,11 +302,8 @@ const deleteSecretValue = ( · 137 unmodified lines
327 return true;302 return true;
328 });303 });
329 304
330const formatVaultError = (error: unknown): StorageError => {305const formatVaultError = (error: WorkOSVaultClientError): StorageError =>
331 const cause = unwrapVaultError(error);306 new StorageError({ message: error.message, cause: error.cause });
332 const message = cause instanceof Error ? cause.message : String(cause);
333 return new StorageError({ message, cause });
334};
335 307
336// ---------------------------------------------------------------------------308// ---------------------------------------------------------------------------
337// makeWorkOSVaultSecretProvider — builds a SecretProvider backed by309// makeWorkOSVaultSecretProvider — builds a SecretProvider backed by
scripts/oxlint-plugin-executor.js+14−0
@@ -2,7 +2,14 @@ import noConditionalTests from "./oxlint-plugin-executor/rules/no-conditional-te · 1 unmodified line
2import noCrossPackageRelativeImports from "./oxlint-plugin-executor/rules/no-cross-package-relative-imports.js";2import noCrossPackageRelativeImports from "./oxlint-plugin-executor/rules/no-cross-package-relative-imports.js";
3import noDoubleCast from "./oxlint-plugin-executor/rules/no-double-cast.js";3import noDoubleCast from "./oxlint-plugin-executor/rules/no-double-cast.js";
4import noEffectInternalTags from "./oxlint-plugin-executor/rules/no-effect-internal-tags.js";4import noEffectInternalTags from "./oxlint-plugin-executor/rules/no-effect-internal-tags.js";
5import noInlineObjectTypeAssertion from "./oxlint-plugin-executor/rules/no-inline-object-type-assertion.js";
6import noInstanceofTaggedError from "./oxlint-plugin-executor/rules/no-instanceof-tagged-error.js";
7import noManualTagCheck from "./oxlint-plugin-executor/rules/no-manual-tag-check.js";
8import noPromiseClientSurface from "./oxlint-plugin-executor/rules/no-promise-client-surface.js";
9import noRawErrorThrow from "./oxlint-plugin-executor/rules/no-raw-error-throw.js";
10import noRedundantErrorFactory from "./oxlint-plugin-executor/rules/no-redundant-error-factory.js";
5import noTsNocheck from "./oxlint-plugin-executor/rules/no-ts-nocheck.js";11import noTsNocheck from "./oxlint-plugin-executor/rules/no-ts-nocheck.js";
12import noUnknownShapeProbing from "./oxlint-plugin-executor/rules/no-unknown-shape-probing.js";
6import noVitestImport from "./oxlint-plugin-executor/rules/no-vitest-import.js";13import noVitestImport from "./oxlint-plugin-executor/rules/no-vitest-import.js";
7import requireReactivityKeys from "./oxlint-plugin-executor/rules/require-reactivity-keys.js";14import requireReactivityKeys from "./oxlint-plugin-executor/rules/require-reactivity-keys.js";
8 15
@@ -18,5 +25,12 @@ export default { · 9 unmodified lines
18 "require-reactivity-keys": requireReactivityKeys,25 "require-reactivity-keys": requireReactivityKeys,
19 "no-effect-internal-tags": noEffectInternalTags,26 "no-effect-internal-tags": noEffectInternalTags,
20 "no-ts-nocheck": noTsNocheck,27 "no-ts-nocheck": noTsNocheck,
28 "no-inline-object-type-assertion": noInlineObjectTypeAssertion,
29 "no-instanceof-tagged-error": noInstanceofTaggedError,
30 "no-manual-tag-check": noManualTagCheck,
31 "no-promise-client-surface": noPromiseClientSurface,
32 "no-raw-error-throw": noRawErrorThrow,
33 "no-redundant-error-factory": noRedundantErrorFactory,
34 "no-unknown-shape-probing": noUnknownShapeProbing,
21 },35 },
22};36};
scripts/oxlint-plugin-executor/rules/no-inline-object-type-assertion.js+40−0
@@ -0,0 +1,40 @@
1import { isIdentifier } from "../utils.js";
2
3const message =
4 "Do not assert against inline object-shaped types. Use a named type, Schema, or a proper type guard.";
5
6const isUnknownKeyword = (node) => node?.type === "TSUnknownKeyword";
7
8const isStringKey = (node) =>
9 node?.type === "TSStringKeyword" ||
10 (node?.type === "TSLiteralType" && typeof node.literal?.value === "string");
11
12const isRecordUnknown = (node) =>
13 node?.type === "TSTypeReference" &&
14 isIdentifier(node.typeName, "Record") &&
15 node.typeArguments?.params?.length === 2 &&
16 isStringKey(node.typeArguments.params[0]) &&
17 isUnknownKeyword(node.typeArguments.params[1]);
18
19const isBannedType = (node) => node?.type === "TSTypeLiteral" || isRecordUnknown(node);
20
21export default {
22 meta: {
23 type: "problem",
24 docs: {
25 description: message,
26 },
27 },
28 create(context) {
29 const check = (node) => {
30 if (isBannedType(node.typeAnnotation)) {
31 context.report({ node, message });
32 }
33 };
34
35 return {
36 TSAsExpression: check,
37 TSTypeAssertion: check,
38 };
39 },
40};
scripts/oxlint-plugin-executor/rules/no-instanceof-tagged-error.js+27−0
@@ -0,0 +1,27 @@
1import { isIdentifier, nodeName } from "../utils.js";
2
3const message =
4 "Do not use instanceof for tagged errors. Use Effect.catchTag, Effect.catchTags, or a _tag-based guard.";
5
6const looksLikeTaggedErrorName = (name) =>
7 typeof name === "string" && name !== "Error" && name.endsWith("Error");
8
9export default {
10 meta: {
11 type: "problem",
12 docs: {
13 description: message,
14 },
15 },
16 create(context) {
17 return {
18 BinaryExpression(node) {
19 if (node.operator !== "instanceof") return;
20 const rightName = nodeName(node.right);
21 if (isIdentifier(node.right) && looksLikeTaggedErrorName(rightName)) {
22 context.report({ node, message });
23 }
24 },
25 };
26 },
27};
scripts/oxlint-plugin-executor/rules/no-manual-tag-check.js+25−0
@@ -0,0 +1,25 @@
1import { isIdentifier, isStringLiteral } from "../utils.js";
2
3const message =
4 "Do not inspect _tag manually. Use Effect.catchTag, Effect.catchTags, Predicate.isTagged, or another Effect tagged-error API.";
5
6const isTagProperty = (node) =>
7 isIdentifier(node, "_tag") || (isStringLiteral(node) && node.value === "_tag");
8
9export default {
10 meta: {
11 type: "problem",
12 docs: {
13 description: message,
14 },
15 },
16 create(context) {
17 return {
18 MemberExpression(node) {
19 if (isTagProperty(node.property)) {
20 context.report({ node, message });
21 }
22 },
23 };
24 },
25};
scripts/oxlint-plugin-executor/rules/no-promise-client-surface.js+42−0
@@ -0,0 +1,42 @@
1import { containsPromiseType, nodeName } from "../utils.js";
2
3const message =
4 "Do not expose Promise-shaped client surfaces. Wrap third-party SDK promises at the adapter boundary and expose Effect methods.";
5
6const isExported = (node) => node?.parent?.type === "ExportNamedDeclaration";
7
8const isClientInterface = (node) => {
9 const name = nodeName(node.id);
10 return (
11 typeof name === "string" &&
12 (name.endsWith("Client") || (isExported(node) && name.endsWith("Sdk")))
13 );
14};
15
16const methodReturnsPromise = (node) => containsPromiseType(node.returnType);
17
18const propertyReturnsPromise = (node) => containsPromiseType(node.typeAnnotation);
19
20export default {
21 meta: {
22 type: "problem",
23 docs: {
24 description: message,
25 },
26 },
27 create(context) {
28 return {
29 TSInterfaceDeclaration(node) {
30 if (!isClientInterface(node)) return;
31 for (const member of node.body?.body ?? []) {
32 if (
33 (member.type === "TSMethodSignature" && methodReturnsPromise(member)) ||
34 (member.type === "TSPropertySignature" && propertyReturnsPromise(member))
35 ) {
36 context.report({ node: member, message });
37 }
38 }
39 },
40 };
41 },
42};
scripts/oxlint-plugin-executor/rules/no-raw-error-throw.js+24−0
@@ -0,0 +1,24 @@
1import { isIdentifier } from "../utils.js";
2
3const message =
4 "Do not throw raw Error objects in Effect code. Return Effect.fail with a tagged error or assert directly in tests.";
5
6const isNewError = (node) => node?.type === "NewExpression" && isIdentifier(node.callee, "Error");
7
8export default {
9 meta: {
10 type: "problem",
11 docs: {
12 description: message,
13 },
14 },
15 create(context) {
16 return {
17 ThrowStatement(node) {
18 if (isNewError(node.argument)) {
19 context.report({ node, message });
20 }
21 },
22 };
23 },
24};
scripts/oxlint-plugin-executor/rules/no-redundant-error-factory.js+52−0
@@ -0,0 +1,52 @@
1import { isIdentifier } from "../utils.js";
2
3const message =
4 "Do not add redundant make*Error wrappers that only construct a tagged error. Construct the tagged error directly.";
5
6const isErrorFactoryName = (name) => /^make[A-Z].*Error$/.test(name);
7
8const isNewErrorExpression = (node) =>
9 node?.type === "NewExpression" && isIdentifier(node.callee) && node.callee.name.endsWith("Error");
10
11const returnsOnlyNewError = (node) => {
12 if (isNewErrorExpression(node)) return true;
13 if (node?.type !== "BlockStatement") return false;
14 const statements = node.body ?? [];
15 return (
16 statements.length === 1 &&
17 statements[0]?.type === "ReturnStatement" &&
18 isNewErrorExpression(statements[0].argument)
19 );
20};
21
22const reportIfRedundantFactory = (context, name, body, node) => {
23 if (isErrorFactoryName(name) && returnsOnlyNewError(body)) {
24 context.report({ node, message });
25 }
26};
27
28export default {
29 meta: {
30 type: "problem",
31 docs: {
32 description: message,
33 },
34 },
35 create(context) {
36 return {
37 FunctionDeclaration(node) {
38 reportIfRedundantFactory(context, node.id?.name, node.body, node);
39 },
40 VariableDeclarator(node) {
41 if (!isIdentifier(node.id)) return;
42 if (
43 node.init?.type !== "ArrowFunctionExpression" &&
44 node.init?.type !== "FunctionExpression"
45 ) {
46 return;
47 }
48 reportIfRedundantFactory(context, node.id.name, node.init.body, node);
49 },
50 };
51 },
52};
scripts/oxlint-plugin-executor/rules/no-unknown-shape-probing.js+32−0
@@ -0,0 +1,32 @@
1import { isIdentifier, isStringLiteral } from "../utils.js";
2
3const message =
4 "Do not probe unknown object shapes in domain code. Normalize at a boundary with Schema, a typed adapter, or a named guard.";
5
6const isReflectGet = (node) =>
7 node?.type === "MemberExpression" &&
8 isIdentifier(node.object, "Reflect") &&
9 isIdentifier(node.property, "get");
10
11export default {
12 meta: {
13 type: "problem",
14 docs: {
15 description: message,
16 },
17 },
18 create(context) {
19 return {
20 CallExpression(node) {
21 if (isReflectGet(node.callee)) {
22 context.report({ node, message });
23 }
24 },
25 BinaryExpression(node) {
26 if (node.operator === "in" && isStringLiteral(node.left)) {
27 context.report({ node, message });
28 }
29 },
30 };
31 },
32};
scripts/oxlint-plugin-executor/utils.js+57−0
@@ -71,3 +71,60 @@ export function getStringValue(node) { · 70 unmodified lines
71 if (expression?.type === "StringLiteral") return expression.value;71 if (expression?.type === "StringLiteral") return expression.value;
72 return undefined;72 return undefined;
73}73}
74
75export function isIdentifier(node, name) {
76 return node?.type === "Identifier" && (name === undefined || node.name === name);
77}
78
79export function isStringLiteral(node) {
80 return (
81 (node?.type === "Literal" && typeof node.value === "string") ||
82 node?.type === "StringLiteral"
83 );
84}
85
86export function typeName(node) {
87 if (node?.type === "Identifier") return node.name;
88 if (node?.type === "TSQualifiedName") {
89 const left = typeName(node.left);
90 const right = typeName(node.right);
91 return left && right ? `${left}.${right}` : undefined;
92 }
93 return undefined;
94}
95
96export function typeReferenceName(node) {
97 return node?.type === "TSTypeReference" ? typeName(node.typeName) : undefined;
98}
99
100export function isPromiseType(node) {
101 return typeReferenceName(node) === "Promise";
102}
103
104export function containsPromiseType(node) {
105 if (!node || typeof node !== "object") return false;
106 if (isPromiseType(node)) return true;
107
108 switch (node.type) {
109 case "TSTypeAnnotation":
110 return containsPromiseType(node.typeAnnotation);
111 case "TSFunctionType":
112 return containsPromiseType(node.returnType);
113 case "TSParenthesizedType":
114 return containsPromiseType(node.typeAnnotation);
115 case "TSUnionType":
116 case "TSIntersectionType":
117 return (node.types ?? []).some(containsPromiseType);
118 case "TSConditionalType":
119 return containsPromiseType(node.trueType) || containsPromiseType(node.falseType);
120 default:
121 return false;
122 }
123}
124
125export function nodeName(node) {
126 if (isIdentifier(node)) return node.name;
127 if (node?.type === "PrivateIdentifier") return node.name;
128 if (isStringLiteral(node)) return node.value;
129 return undefined;
130}