From c517ca6fb972c3f6385dda2939a85f98ccf53c6f Mon Sep 17 00:00:00 2001 From: Mattia Manzati Date: Thu, 23 Jul 2026 08:47:07 +0200 Subject: [PATCH] Handle local Effect provides --- .changeset/local-effect-provides.md | 5 ++ .../multipleEffectProvide.ts.codefixes | 5 +- ...pleEffectProvide_fix.from1339to1373.output | 49 +++++++++++++++++++ ...tipleEffectProvide_fix.from592to626.output | 11 +++++ ...tipleEffectProvide_fix.from726to760.output | 11 +++++ ...tipleEffectProvide_fix.from819to853.output | 11 +++++ ...tipleEffectProvide_fix.from949to983.output | 11 +++++ .../multipleEffectProvide.ts.output | 5 +- .../diagnostics/multipleEffectProvide.ts | 11 +++++ .../src/diagnostics/multipleEffectProvide.ts | 18 +++++++ 10 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 .changeset/local-effect-provides.md create mode 100644 packages/harness-effect-v4/__snapshots__/diagnostics/multipleEffectProvide.ts.multipleEffectProvide_fix.from1339to1373.output diff --git a/.changeset/local-effect-provides.md b/.changeset/local-effect-provides.md new file mode 100644 index 00000000..0b3a9977 --- /dev/null +++ b/.changeset/local-effect-provides.md @@ -0,0 +1,5 @@ +--- +"@effect/language-service": patch +--- + +Ignore Effect v4 local `Effect.provide(..., { local: true })` calls when reporting chained provides with the `multipleEffectProvide` diagnostic. diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/multipleEffectProvide.ts.codefixes b/packages/harness-effect-v4/__snapshots__/diagnostics/multipleEffectProvide.ts.codefixes index 9cfd6f1a..2cb4e6bb 100644 --- a/packages/harness-effect-v4/__snapshots__/diagnostics/multipleEffectProvide.ts.codefixes +++ b/packages/harness-effect-v4/__snapshots__/diagnostics/multipleEffectProvide.ts.codefixes @@ -9,4 +9,7 @@ multipleEffectProvide_skipNextLine from 819 to 853 multipleEffectProvide_skipFile from 819 to 853 multipleEffectProvide_fix from 949 to 983 multipleEffectProvide_skipNextLine from 949 to 983 -multipleEffectProvide_skipFile from 949 to 983 \ No newline at end of file +multipleEffectProvide_skipFile from 949 to 983 +multipleEffectProvide_fix from 1339 to 1373 +multipleEffectProvide_skipNextLine from 1339 to 1373 +multipleEffectProvide_skipFile from 1339 to 1373 \ No newline at end of file diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/multipleEffectProvide.ts.multipleEffectProvide_fix.from1339to1373.output b/packages/harness-effect-v4/__snapshots__/diagnostics/multipleEffectProvide.ts.multipleEffectProvide_fix.from1339to1373.output new file mode 100644 index 00000000..0459591a --- /dev/null +++ b/packages/harness-effect-v4/__snapshots__/diagnostics/multipleEffectProvide.ts.multipleEffectProvide_fix.from1339to1373.output @@ -0,0 +1,49 @@ +// code fix multipleEffectProvide_fix output for range 1339 - 1373 +import { Context, Effect, Layer } from "effect" + +class MyService1 extends Context.Service()("MyService1", { + make: Effect.succeed({ value: 1 }) +}) { + static Default = Layer.effect(this, this.make) +} + +class MyService2 extends Context.Service()("MyService2", { + make: Effect.succeed({ value: 2 }) +}) { + static Default = Layer.effect(this, this.make) +} + +class MyService3 extends Context.Service()("MyService3", { + make: Effect.succeed({ value: 3 }) +}) { + static Default = Layer.effect(this, this.make) +} + +export const shouldReport = Effect.void.pipe( + Effect.provide(MyService1.Default), + Effect.provide(MyService2.Default) +) + +export const shouldReportSeparately = Effect.void.pipe( + Effect.provide(MyService1.Default), + Effect.provide(MyService2.Default), + Effect.ignore, + Effect.provide(MyService1.Default), + Effect.provide(MyService2.Default) +) + +export const shouldReportSingle = Effect.void.pipe( + Effect.provide(MyService1.Default), + Effect.provide(MyService2.Default), + Effect.provide(MyService3.Default) +) + +export const shouldNotReportLocalProvide = Effect.void.pipe( + Effect.provide(MyService1.Default), + Effect.provide(MyService2.Default, { local: true }) +) + +export const shouldReportAfterLocalProvide = Effect.void.pipe( + Effect.provide(MyService1.Default, { local: true }), + Effect.provide(Layer.mergeAll(MyService2.Default, MyService3.Default)) +) diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/multipleEffectProvide.ts.multipleEffectProvide_fix.from592to626.output b/packages/harness-effect-v4/__snapshots__/diagnostics/multipleEffectProvide.ts.multipleEffectProvide_fix.from592to626.output index 80527897..28d35e40 100644 --- a/packages/harness-effect-v4/__snapshots__/diagnostics/multipleEffectProvide.ts.multipleEffectProvide_fix.from592to626.output +++ b/packages/harness-effect-v4/__snapshots__/diagnostics/multipleEffectProvide.ts.multipleEffectProvide_fix.from592to626.output @@ -36,3 +36,14 @@ export const shouldReportSingle = Effect.void.pipe( Effect.provide(MyService2.Default), Effect.provide(MyService3.Default) ) + +export const shouldNotReportLocalProvide = Effect.void.pipe( + Effect.provide(MyService1.Default), + Effect.provide(MyService2.Default, { local: true }) +) + +export const shouldReportAfterLocalProvide = Effect.void.pipe( + Effect.provide(MyService1.Default, { local: true }), + Effect.provide(MyService2.Default), + Effect.provide(MyService3.Default) +) diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/multipleEffectProvide.ts.multipleEffectProvide_fix.from726to760.output b/packages/harness-effect-v4/__snapshots__/diagnostics/multipleEffectProvide.ts.multipleEffectProvide_fix.from726to760.output index 80ad1f17..dbefcace 100644 --- a/packages/harness-effect-v4/__snapshots__/diagnostics/multipleEffectProvide.ts.multipleEffectProvide_fix.from726to760.output +++ b/packages/harness-effect-v4/__snapshots__/diagnostics/multipleEffectProvide.ts.multipleEffectProvide_fix.from726to760.output @@ -36,3 +36,14 @@ export const shouldReportSingle = Effect.void.pipe( Effect.provide(MyService2.Default), Effect.provide(MyService3.Default) ) + +export const shouldNotReportLocalProvide = Effect.void.pipe( + Effect.provide(MyService1.Default), + Effect.provide(MyService2.Default, { local: true }) +) + +export const shouldReportAfterLocalProvide = Effect.void.pipe( + Effect.provide(MyService1.Default, { local: true }), + Effect.provide(MyService2.Default), + Effect.provide(MyService3.Default) +) diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/multipleEffectProvide.ts.multipleEffectProvide_fix.from819to853.output b/packages/harness-effect-v4/__snapshots__/diagnostics/multipleEffectProvide.ts.multipleEffectProvide_fix.from819to853.output index a90dfb8b..feef817a 100644 --- a/packages/harness-effect-v4/__snapshots__/diagnostics/multipleEffectProvide.ts.multipleEffectProvide_fix.from819to853.output +++ b/packages/harness-effect-v4/__snapshots__/diagnostics/multipleEffectProvide.ts.multipleEffectProvide_fix.from819to853.output @@ -36,3 +36,14 @@ export const shouldReportSingle = Effect.void.pipe( Effect.provide(MyService2.Default), Effect.provide(MyService3.Default) ) + +export const shouldNotReportLocalProvide = Effect.void.pipe( + Effect.provide(MyService1.Default), + Effect.provide(MyService2.Default, { local: true }) +) + +export const shouldReportAfterLocalProvide = Effect.void.pipe( + Effect.provide(MyService1.Default, { local: true }), + Effect.provide(MyService2.Default), + Effect.provide(MyService3.Default) +) diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/multipleEffectProvide.ts.multipleEffectProvide_fix.from949to983.output b/packages/harness-effect-v4/__snapshots__/diagnostics/multipleEffectProvide.ts.multipleEffectProvide_fix.from949to983.output index abbec6f8..fa879812 100644 --- a/packages/harness-effect-v4/__snapshots__/diagnostics/multipleEffectProvide.ts.multipleEffectProvide_fix.from949to983.output +++ b/packages/harness-effect-v4/__snapshots__/diagnostics/multipleEffectProvide.ts.multipleEffectProvide_fix.from949to983.output @@ -35,3 +35,14 @@ export const shouldReportSeparately = Effect.void.pipe( export const shouldReportSingle = Effect.void.pipe( Effect.provide(Layer.mergeAll(MyService1.Default, MyService2.Default, MyService3.Default)) ) + +export const shouldNotReportLocalProvide = Effect.void.pipe( + Effect.provide(MyService1.Default), + Effect.provide(MyService2.Default, { local: true }) +) + +export const shouldReportAfterLocalProvide = Effect.void.pipe( + Effect.provide(MyService1.Default, { local: true }), + Effect.provide(MyService2.Default), + Effect.provide(MyService3.Default) +) diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/multipleEffectProvide.ts.output b/packages/harness-effect-v4/__snapshots__/diagnostics/multipleEffectProvide.ts.output index 3837d545..c1b2c826 100644 --- a/packages/harness-effect-v4/__snapshots__/diagnostics/multipleEffectProvide.ts.output +++ b/packages/harness-effect-v4/__snapshots__/diagnostics/multipleEffectProvide.ts.output @@ -8,4 +8,7 @@ Effect.provide(MyService1.Default) 30:2 - 30:36 | 0 | This expression chains multiple `Effect.provide` calls. Providing Layers in multiple calls in a chain can break service lifecycle behavior compared with a single combined provide with merged layers. effect(multipleEffectProvide) Effect.provide(MyService1.Default) -35:2 - 35:36 | 0 | This expression chains multiple `Effect.provide` calls. Providing Layers in multiple calls in a chain can break service lifecycle behavior compared with a single combined provide with merged layers. effect(multipleEffectProvide) \ No newline at end of file +35:2 - 35:36 | 0 | This expression chains multiple `Effect.provide` calls. Providing Layers in multiple calls in a chain can break service lifecycle behavior compared with a single combined provide with merged layers. effect(multipleEffectProvide) + +Effect.provide(MyService2.Default) +47:2 - 47:36 | 0 | This expression chains multiple `Effect.provide` calls. Providing Layers in multiple calls in a chain can break service lifecycle behavior compared with a single combined provide with merged layers. effect(multipleEffectProvide) \ No newline at end of file diff --git a/packages/harness-effect-v4/examples/diagnostics/multipleEffectProvide.ts b/packages/harness-effect-v4/examples/diagnostics/multipleEffectProvide.ts index 420d60c5..76a00ff3 100644 --- a/packages/harness-effect-v4/examples/diagnostics/multipleEffectProvide.ts +++ b/packages/harness-effect-v4/examples/diagnostics/multipleEffectProvide.ts @@ -36,3 +36,14 @@ export const shouldReportSingle = Effect.void.pipe( Effect.provide(MyService2.Default), Effect.provide(MyService3.Default) ) + +export const shouldNotReportLocalProvide = Effect.void.pipe( + Effect.provide(MyService1.Default), + Effect.provide(MyService2.Default, { local: true }) +) + +export const shouldReportAfterLocalProvide = Effect.void.pipe( + Effect.provide(MyService1.Default, { local: true }), + Effect.provide(MyService2.Default), + Effect.provide(MyService3.Default) +) diff --git a/packages/language-service/src/diagnostics/multipleEffectProvide.ts b/packages/language-service/src/diagnostics/multipleEffectProvide.ts index 6721abdd..26855b6e 100644 --- a/packages/language-service/src/diagnostics/multipleEffectProvide.ts +++ b/packages/language-service/src/diagnostics/multipleEffectProvide.ts @@ -21,6 +21,7 @@ export const multipleEffectProvide = LSP.createDiagnostic({ const tsUtils = yield* Nano.service(TypeScriptUtils.TypeScriptUtils) const typeCheckerUtils = yield* Nano.service(TypeCheckerUtils.TypeCheckerUtils) const typeParser = yield* Nano.service(TypeParser.TypeParser) + const supportedEffect = typeParser.supportedEffect() const effectModuleIdentifier = tsUtils.findImportedModuleIdentifierByPackageAndNameOrBarrel( sourceFile, @@ -58,6 +59,23 @@ export const multipleEffectProvide = LSP.createDiagnostic({ ) if (Option.isSome(isProvideCall)) { + const options = transformation.args[1] + const isLocalProvide = supportedEffect === "v4" && + options !== undefined && + ts.isObjectLiteralExpression(options) && + options.properties.some((property) => + ts.isPropertyAssignment(property) && + ts.isIdentifier(property.name) && + ts.idText(property.name) === "local" && + property.initializer.kind === ts.SyntaxKind.TrueKeyword + ) + + if (isLocalProvide) { + currentChunk++ + previousLayers.push([]) + continue + } + const layer = transformation.args[0] const type = typeCheckerUtils.getTypeAtLocation(layer) const node = ts.findAncestor(transformation.callee, ts.isCallExpression)