diff --git a/.oxlintrc.jsonc b/.oxlintrc.jsonc index 9024842..1889d48 100644 --- a/.oxlintrc.jsonc +++ b/.oxlintrc.jsonc @@ -16,6 +16,7 @@ "automation/no-silent-error-swallow": "error", "automation/no-manual-tag-comparison": "error", "automation/no-manual-tagged-construction": "error", + "automation/no-service-option": "error", "automation/prefer-effect-match": "error", "automation/prefer-tagged-error-handling": "error", "automation/no-ambient-nondeterminism": "error", @@ -78,6 +79,7 @@ "automation/no-silent-error-swallow": "off", "automation/no-manual-tag-comparison": "off", "automation/no-manual-tagged-construction": "off", + "automation/no-service-option": "off", "automation/prefer-effect-match": "off", "automation/prefer-tagged-error-handling": "off", "automation/no-ambient-nondeterminism": "off", @@ -99,6 +101,14 @@ "automation/no-manual-tagged-construction": "off", }, }, + { + // Tests may inspect whether optional provider override services are present in context. This verifies + // request-scoping behavior; it does not model an optional production dependency. + "files": ["packages/*/test/**"], + "rules": { + "automation/no-service-option": "off", + }, + }, { // no-ambient-nondeterminism targets production seams. Tests deliberately use fixed or real // time and node crypto, driven through explicit layers, so the rule is off for test files. diff --git a/tools/oxlint/automation/index.ts b/tools/oxlint/automation/index.ts index 032763e..f953c35 100644 --- a/tools/oxlint/automation/index.ts +++ b/tools/oxlint/automation/index.ts @@ -4,6 +4,7 @@ import noAmbientNondeterminism from './rules/no-ambient-nondeterminism.ts' import noDisableValidation from './rules/no-disable-validation.ts' import noManualTagComparison from './rules/no-manual-tag-comparison.ts' import noManualTaggedConstruction from './rules/no-manual-tagged-construction.ts' +import noServiceOption from './rules/no-service-option.ts' import noShadowedStandardArrayStatic from './rules/no-shadowed-standard-array-static.ts' import noSilentErrorSwallow from './rules/no-silent-error-swallow.ts' import preferEffectMatch from './rules/prefer-effect-match.ts' @@ -16,6 +17,7 @@ export default eslintCompatPlugin({ 'no-disable-validation': noDisableValidation, 'no-manual-tag-comparison': noManualTagComparison, 'no-manual-tagged-construction': noManualTaggedConstruction, + 'no-service-option': noServiceOption, 'no-shadowed-standard-array-static': noShadowedStandardArrayStatic, 'no-silent-error-swallow': noSilentErrorSwallow, 'prefer-effect-match': preferEffectMatch, diff --git a/tools/oxlint/automation/rules/no-service-option.ts b/tools/oxlint/automation/rules/no-service-option.ts new file mode 100644 index 0000000..34373fa --- /dev/null +++ b/tools/oxlint/automation/rules/no-service-option.ts @@ -0,0 +1,32 @@ +import { defineRule } from '@oxlint/plugins' + +// Vendored and adapted from typeonce-dev/ai-automation (rules/oxlint/src/rules/no-service-option.ts). +// This rule protects production dependency boundaries. Tests that explicitly inspect whether a service is +// present are excluded through `.oxlintrc.jsonc` rather than weakening the production rule. + +export default defineRule({ + meta: { + type: 'problem', + docs: { + description: 'Require Effect services directly and provide them at the owning layer or composition root.', + }, + messages: { + optionalService: + 'Do not use Effect.serviceOption for production dependencies. Require the service directly and provide it in the layer.', + }, + }, + createOnce(context) { + return { + MemberExpression(node) { + if ( + node.object.type === 'Identifier' && + node.object.name === 'Effect' && + node.property.type === 'Identifier' && + node.property.name === 'serviceOption' + ) { + context.report({ node, messageId: 'optionalService' }) + } + }, + } + }, +})